From b2d35c528654046eaed5d5d32099de32384400ae Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Tue, 15 Oct 2024 17:46:52 +0300 Subject: [PATCH 001/105] V5 bringing RESP3, Sentinel and TypeMapping to node-redis RESP3 Support - Some commands responses in RESP3 aren't stable yet and therefore return an "untyped" ReplyUnion. Sentinel TypeMapping Correctly types Multi commands Note: some API changes to be further documented in v4-to-v5.md --- .github/release-drafter-base.yml | 50 + .github/workflows/documentation.yml | 2 - .github/workflows/tests.yml | 11 +- .gitignore | 1 + .npmignore | 12 - README.md | 346 +- benchmark/lib/index.js | 10 +- benchmark/lib/ping/ioredis-auto-pipeline.js | 20 + benchmark/lib/ping/local-resp2.js | 21 + .../lib/ping/local-resp3-buffer-proxy.js | 23 + benchmark/lib/ping/local-resp3-buffer.js | 24 + .../lib/ping/local-resp3-module-with-flags.js | 27 + benchmark/lib/ping/local-resp3-module.js | 27 + benchmark/lib/ping/local-resp3.js | 21 + benchmark/lib/ping/v3.js | 4 +- benchmark/lib/ping/v4.js | 2 +- benchmark/lib/runner.js | 6 +- benchmark/lib/set-get-delete-string/index.js | 2 +- benchmark/lib/set-get-delete-string/v3.js | 4 +- benchmark/package-lock.json | 87 +- benchmark/package.json | 9 +- docs/RESP.md | 46 + docs/client-configuration.md | 64 +- docs/clustering.md | 30 +- docs/command-options.md | 68 + docs/isolated-execution.md | 67 - docs/pool.md | 74 + docs/programmability.md | 76 + docs/pub-sub.md | 14 +- docs/scan-iterators.md | 30 + docs/sentinel.md | 100 + docs/todo.md | 6 + docs/transactions.md | 53 + docs/v4-to-v5.md | 240 + docs/v5.md | 38 + examples/README.md | 2 +- examples/blocking-list-pop.js | 2 +- examples/bloom-filter.js | 2 +- examples/check-connection-status.js | 2 +- examples/command-with-modifiers.js | 38 +- examples/connect-as-acl-user.js | 2 +- examples/count-min-sketch.js | 2 +- examples/cuckoo-filter.js | 2 +- examples/dump-and-restore.js | 22 +- examples/get-server-time.js | 2 +- examples/hyperloglog.js | 2 +- examples/lua-multi-incr.js | 2 +- examples/managing-json.js | 2 +- examples/package.json | 2 +- examples/search-hashes.js | 2 +- examples/search-json.js | 2 +- examples/search-knn.js | 2 +- examples/set-scan.js | 2 +- examples/sorted-set.js | 2 +- examples/stream-producer.js | 2 +- examples/time-series.js | 2 +- examples/topk.js | 2 +- .../transaction-with-arbitrary-commands.js | 2 +- index.ts | 77 - package-lock.json | 4613 +++++++---------- package.json | 55 +- packages/bloom/.npmignore | 6 - packages/bloom/.release-it.json | 1 + packages/bloom/README.md | 11 +- packages/bloom/lib/commands/bloom/ADD.spec.ts | 30 +- packages/bloom/lib/commands/bloom/ADD.ts | 14 +- .../bloom/lib/commands/bloom/CARD.spec.ts | 30 +- packages/bloom/lib/commands/bloom/CARD.ts | 15 +- .../bloom/lib/commands/bloom/EXISTS.spec.ts | 30 +- packages/bloom/lib/commands/bloom/EXISTS.ts | 16 +- .../bloom/lib/commands/bloom/INFO.spec.ts | 40 +- packages/bloom/lib/commands/bloom/INFO.ts | 62 +- .../bloom/lib/commands/bloom/INSERT.spec.ts | 116 +- packages/bloom/lib/commands/bloom/INSERT.ts | 58 +- .../lib/commands/bloom/LOADCHUNK.spec.ts | 53 +- .../bloom/lib/commands/bloom/LOADCHUNK.ts | 21 +- .../bloom/lib/commands/bloom/MADD.spec.ts | 30 +- packages/bloom/lib/commands/bloom/MADD.ts | 17 +- .../bloom/lib/commands/bloom/MEXISTS.spec.ts | 30 +- packages/bloom/lib/commands/bloom/MEXISTS.ts | 19 +- .../bloom/lib/commands/bloom/RESERVE.spec.ts | 82 +- packages/bloom/lib/commands/bloom/RESERVE.ts | 25 +- .../bloom/lib/commands/bloom/SCANDUMP.spec.ts | 36 +- packages/bloom/lib/commands/bloom/SCANDUMP.ts | 31 +- packages/bloom/lib/commands/bloom/index.ts | 93 +- .../commands/count-min-sketch/INCRBY.spec.ts | 71 +- .../lib/commands/count-min-sketch/INCRBY.ts | 37 +- .../commands/count-min-sketch/INFO.spec.ts | 43 +- .../lib/commands/count-min-sketch/INFO.ts | 52 +- .../count-min-sketch/INITBYDIM.spec.ts | 30 +- .../commands/count-min-sketch/INITBYDIM.ts | 13 +- .../count-min-sketch/INITBYPROB.spec.ts | 30 +- .../commands/count-min-sketch/INITBYPROB.ts | 13 +- .../commands/count-min-sketch/MERGE.spec.ts | 56 +- .../lib/commands/count-min-sketch/MERGE.ts | 52 +- .../commands/count-min-sketch/QUERY.spec.ts | 33 +- .../lib/commands/count-min-sketch/QUERY.ts | 26 +- .../lib/commands/count-min-sketch/index.ts | 39 +- .../bloom/lib/commands/cuckoo/ADD.spec.ts | 30 +- packages/bloom/lib/commands/cuckoo/ADD.ts | 14 +- .../bloom/lib/commands/cuckoo/ADDNX.spec.ts | 32 +- packages/bloom/lib/commands/cuckoo/ADDNX.ts | 14 +- .../bloom/lib/commands/cuckoo/COUNT.spec.ts | 30 +- packages/bloom/lib/commands/cuckoo/COUNT.ts | 13 +- .../bloom/lib/commands/cuckoo/DEL.spec.ts | 32 +- packages/bloom/lib/commands/cuckoo/DEL.ts | 14 +- .../bloom/lib/commands/cuckoo/EXISTS.spec.ts | 30 +- packages/bloom/lib/commands/cuckoo/EXISTS.ts | 16 +- .../bloom/lib/commands/cuckoo/INFO.spec.ts | 46 +- packages/bloom/lib/commands/cuckoo/INFO.ts | 71 +- .../bloom/lib/commands/cuckoo/INSERT.spec.ts | 36 +- packages/bloom/lib/commands/cuckoo/INSERT.ts | 44 +- .../lib/commands/cuckoo/INSERTNX.spec.ts | 36 +- .../bloom/lib/commands/cuckoo/INSERTNX.ts | 25 +- .../lib/commands/cuckoo/LOADCHUNK.spec.ts | 57 +- .../bloom/lib/commands/cuckoo/LOADCHUNK.ts | 19 +- .../bloom/lib/commands/cuckoo/RESERVE.spec.ts | 80 +- packages/bloom/lib/commands/cuckoo/RESERVE.ts | 39 +- .../lib/commands/cuckoo/SCANDUMP.spec.ts | 39 +- .../bloom/lib/commands/cuckoo/SCANDUMP.ts | 29 +- .../bloom/lib/commands/cuckoo/index.spec.ts | 48 - packages/bloom/lib/commands/cuckoo/index.ts | 95 +- packages/bloom/lib/commands/index.ts | 13 +- .../bloom/lib/commands/t-digest/ADD.spec.ts | 30 +- packages/bloom/lib/commands/t-digest/ADD.ts | 23 +- .../lib/commands/t-digest/BYRANK.spec.ts | 30 +- .../bloom/lib/commands/t-digest/BYRANK.ts | 33 +- .../lib/commands/t-digest/BYREVRANK.spec.ts | 30 +- .../bloom/lib/commands/t-digest/BYREVRANK.ts | 28 +- .../bloom/lib/commands/t-digest/CDF.spec.ts | 30 +- packages/bloom/lib/commands/t-digest/CDF.ts | 24 +- .../lib/commands/t-digest/CREATE.spec.ts | 46 +- .../bloom/lib/commands/t-digest/CREATE.ts | 30 +- .../bloom/lib/commands/t-digest/INFO.spec.ts | 43 +- packages/bloom/lib/commands/t-digest/INFO.ts | 75 +- .../bloom/lib/commands/t-digest/MAX.spec.ts | 30 +- packages/bloom/lib/commands/t-digest/MAX.ts | 25 +- .../bloom/lib/commands/t-digest/MERGE.spec.ts | 80 +- packages/bloom/lib/commands/t-digest/MERGE.ts | 42 +- .../bloom/lib/commands/t-digest/MIN.spec.ts | 30 +- packages/bloom/lib/commands/t-digest/MIN.ts | 25 +- .../lib/commands/t-digest/QUANTILE.spec.ts | 36 +- .../bloom/lib/commands/t-digest/QUANTILE.ts | 28 +- .../bloom/lib/commands/t-digest/RANK.spec.ts | 30 +- packages/bloom/lib/commands/t-digest/RANK.ts | 31 +- .../bloom/lib/commands/t-digest/RESET.spec.ts | 30 +- packages/bloom/lib/commands/t-digest/RESET.ts | 15 +- .../lib/commands/t-digest/REVRANK.spec.ts | 30 +- .../bloom/lib/commands/t-digest/REVRANK.ts | 28 +- .../commands/t-digest/TRIMMED_MEAN.spec.ts | 32 +- .../lib/commands/t-digest/TRIMMED_MEAN.ts | 30 +- .../bloom/lib/commands/t-digest/index.spec.ts | 55 - packages/bloom/lib/commands/t-digest/index.ts | 123 +- packages/bloom/lib/commands/top-k/ADD.spec.ts | 33 +- packages/bloom/lib/commands/top-k/ADD.ts | 22 +- .../bloom/lib/commands/top-k/COUNT.spec.ts | 32 +- packages/bloom/lib/commands/top-k/COUNT.ts | 26 +- .../bloom/lib/commands/top-k/INCRBY.spec.ts | 70 +- packages/bloom/lib/commands/top-k/INCRBY.ts | 41 +- .../bloom/lib/commands/top-k/INFO.spec.ts | 37 +- packages/bloom/lib/commands/top-k/INFO.ts | 52 +- .../bloom/lib/commands/top-k/LIST.spec.ts | 32 +- packages/bloom/lib/commands/top-k/LIST.ts | 15 +- .../lib/commands/top-k/LIST_WITHCOUNT.spec.ts | 47 +- .../lib/commands/top-k/LIST_WITHCOUNT.ts | 38 +- .../bloom/lib/commands/top-k/QUERY.spec.ts | 32 +- packages/bloom/lib/commands/top-k/QUERY.ts | 26 +- .../bloom/lib/commands/top-k/RESERVE.spec.ts | 52 +- packages/bloom/lib/commands/top-k/RESERVE.ts | 37 +- packages/bloom/lib/commands/top-k/index.ts | 51 +- packages/bloom/lib/test-utils.ts | 20 +- packages/bloom/package.json | 28 +- packages/client/.eslintrc.json | 15 - packages/client/.npmignore | 10 - packages/client/.release-it.json | 1 + packages/client/index.ts | 33 +- packages/client/lib/RESP/decoder.spec.ts | 426 ++ packages/client/lib/RESP/decoder.ts | 1178 +++++ packages/client/lib/RESP/encoder.spec.ts | 33 + packages/client/lib/RESP/encoder.ts | 28 + packages/client/lib/RESP/types.ts | 398 ++ packages/client/lib/RESP/verbatim-string.ts | 8 + .../lib/client/RESP2/composers/buffer.spec.ts | 14 - .../lib/client/RESP2/composers/buffer.ts | 18 - .../lib/client/RESP2/composers/interface.ts | 7 - .../lib/client/RESP2/composers/string.spec.ts | 14 - .../lib/client/RESP2/composers/string.ts | 22 - .../client/lib/client/RESP2/decoder.spec.ts | 195 - packages/client/lib/client/RESP2/decoder.ts | 257 - .../client/lib/client/RESP2/encoder.spec.ts | 33 - packages/client/lib/client/RESP2/encoder.ts | 28 - packages/client/lib/client/commands-queue.ts | 615 ++- packages/client/lib/client/commands.ts | 374 -- packages/client/lib/client/index.spec.ts | 1684 +++--- packages/client/lib/client/index.ts | 1782 ++++--- .../client/lib/client/legacy-mode.spec.ts | 111 + packages/client/lib/client/legacy-mode.ts | 177 + .../client/lib/client/linked-list.spec.ts | 138 + packages/client/lib/client/linked-list.ts | 195 + packages/client/lib/client/multi-command.ts | 363 +- packages/client/lib/client/pool.spec.ts | 11 + packages/client/lib/client/pool.ts | 489 ++ packages/client/lib/client/pub-sub.spec.ts | 268 +- packages/client/lib/client/pub-sub.ts | 711 +-- packages/client/lib/client/socket.spec.ts | 162 +- packages/client/lib/client/socket.ts | 537 +- packages/client/lib/cluster/cluster-slots.ts | 1021 ++-- packages/client/lib/cluster/commands.ts | 670 --- packages/client/lib/cluster/index.spec.ts | 661 ++- packages/client/lib/cluster/index.ts | 1053 ++-- packages/client/lib/cluster/multi-command.ts | 355 +- packages/client/lib/command-options.ts | 14 - packages/client/lib/commander.ts | 237 +- packages/client/lib/commands/ACL_CAT.spec.ts | 42 +- packages/client/lib/commands/ACL_CAT.ts | 17 +- .../client/lib/commands/ACL_DELUSER.spec.ts | 44 +- packages/client/lib/commands/ACL_DELUSER.ts | 19 +- .../client/lib/commands/ACL_DRYRUN.spec.ts | 30 +- packages/client/lib/commands/ACL_DRYRUN.ts | 26 +- .../client/lib/commands/ACL_GENPASS.spec.ts | 41 +- packages/client/lib/commands/ACL_GENPASS.ts | 14 +- .../client/lib/commands/ACL_GETUSER.spec.ts | 50 +- packages/client/lib/commands/ACL_GETUSER.ts | 79 +- packages/client/lib/commands/ACL_LIST.spec.ts | 28 +- packages/client/lib/commands/ACL_LIST.ts | 13 +- packages/client/lib/commands/ACL_LOAD.spec.ts | 20 +- packages/client/lib/commands/ACL_LOAD.ts | 13 +- packages/client/lib/commands/ACL_LOG.spec.ts | 89 +- packages/client/lib/commands/ACL_LOG.ts | 90 +- .../client/lib/commands/ACL_LOG_RESET.spec.ts | 27 +- packages/client/lib/commands/ACL_LOG_RESET.ts | 14 +- packages/client/lib/commands/ACL_SAVE.spec.ts | 20 +- packages/client/lib/commands/ACL_SAVE.ts | 13 +- .../client/lib/commands/ACL_SETUSER.spec.ts | 32 +- packages/client/lib/commands/ACL_SETUSER.ts | 20 +- .../client/lib/commands/ACL_USERS.spec.ts | 18 +- packages/client/lib/commands/ACL_USERS.ts | 13 +- .../client/lib/commands/ACL_WHOAMI.spec.ts | 18 +- packages/client/lib/commands/ACL_WHOAMI.ts | 13 +- packages/client/lib/commands/APPEND.spec.ts | 27 +- packages/client/lib/commands/APPEND.ts | 18 +- packages/client/lib/commands/ASKING.spec.ts | 16 +- packages/client/lib/commands/ASKING.ts | 13 +- packages/client/lib/commands/AUTH.spec.ts | 40 +- packages/client/lib/commands/AUTH.ts | 25 +- .../client/lib/commands/BGREWRITEAOF.spec.ts | 24 +- packages/client/lib/commands/BGREWRITEAOF.ts | 13 +- packages/client/lib/commands/BGSAVE.spec.ts | 45 +- packages/client/lib/commands/BGSAVE.ts | 21 +- packages/client/lib/commands/BITCOUNT.spec.ts | 75 +- packages/client/lib/commands/BITCOUNT.ts | 44 +- packages/client/lib/commands/BITFIELD.spec.ts | 91 +- packages/client/lib/commands/BITFIELD.ts | 115 +- .../client/lib/commands/BITFIELD_RO.spec.ts | 47 +- packages/client/lib/commands/BITFIELD_RO.ts | 31 +- packages/client/lib/commands/BITOP.spec.ts | 52 +- packages/client/lib/commands/BITOP.ts | 27 +- packages/client/lib/commands/BITPOS.spec.ts | 76 +- packages/client/lib/commands/BITPOS.ts | 31 +- packages/client/lib/commands/BLMOVE.spec.ts | 66 +- packages/client/lib/commands/BLMOVE.ts | 37 +- packages/client/lib/commands/BLMPOP.spec.ts | 67 +- packages/client/lib/commands/BLMPOP.ts | 29 +- packages/client/lib/commands/BLPOP.spec.ts | 113 +- packages/client/lib/commands/BLPOP.ts | 40 +- packages/client/lib/commands/BRPOP.spec.ts | 113 +- packages/client/lib/commands/BRPOP.ts | 26 +- .../client/lib/commands/BRPOPLPUSH.spec.ts | 77 +- packages/client/lib/commands/BRPOPLPUSH.ts | 21 +- packages/client/lib/commands/BZMPOP.spec.ts | 73 +- packages/client/lib/commands/BZMPOP.ts | 29 +- packages/client/lib/commands/BZPOPMAX.spec.ts | 100 +- packages/client/lib/commands/BZPOPMAX.ts | 64 +- packages/client/lib/commands/BZPOPMIN.spec.ts | 100 +- packages/client/lib/commands/BZPOPMIN.ts | 27 +- .../lib/commands/CLIENT_CACHING.spec.ts | 30 +- .../client/lib/commands/CLIENT_CACHING.ts | 19 +- .../lib/commands/CLIENT_GETNAME.spec.ts | 24 +- .../client/lib/commands/CLIENT_GETNAME.ts | 18 +- .../lib/commands/CLIENT_GETREDIR.spec.ts | 16 +- .../client/lib/commands/CLIENT_GETREDIR.ts | 15 +- .../client/lib/commands/CLIENT_ID.spec.ts | 28 +- packages/client/lib/commands/CLIENT_ID.ts | 13 +- .../client/lib/commands/CLIENT_INFO.spec.ts | 84 +- packages/client/lib/commands/CLIENT_INFO.ts | 154 +- .../client/lib/commands/CLIENT_KILL.spec.ts | 214 +- packages/client/lib/commands/CLIENT_KILL.ts | 163 +- .../client/lib/commands/CLIENT_LIST.spec.ts | 129 +- packages/client/lib/commands/CLIENT_LIST.ts | 57 +- .../lib/commands/CLIENT_NO-EVICT.spec.ts | 44 +- .../client/lib/commands/CLIENT_NO-EVICT.ts | 19 +- .../lib/commands/CLIENT_NO-TOUCH.spec.ts | 42 +- .../client/lib/commands/CLIENT_NO-TOUCH.ts | 18 +- .../client/lib/commands/CLIENT_PAUSE.spec.ts | 42 +- packages/client/lib/commands/CLIENT_PAUSE.ts | 24 +- .../lib/commands/CLIENT_SETNAME.spec.ts | 25 +- .../client/lib/commands/CLIENT_SETNAME.ts | 13 +- .../lib/commands/CLIENT_TRACKING.spec.ts | 172 +- .../client/lib/commands/CLIENT_TRACKING.ts | 98 +- .../lib/commands/CLIENT_TRACKINGINFO.spec.ts | 38 +- .../lib/commands/CLIENT_TRACKINGINFO.ts | 47 +- .../lib/commands/CLIENT_UNPAUSE.spec.ts | 30 +- .../client/lib/commands/CLIENT_UNPAUSE.ts | 13 +- .../lib/commands/CLUSTER_ADDSLOTS.spec.ts | 30 +- .../client/lib/commands/CLUSTER_ADDSLOTS.ts | 21 +- .../commands/CLUSTER_ADDSLOTSRANGE.spec.ts | 51 +- .../lib/commands/CLUSTER_ADDSLOTSRANGE.ts | 19 +- .../lib/commands/CLUSTER_BUMPEPOCH.spec.ts | 30 +- .../client/lib/commands/CLUSTER_BUMPEPOCH.ts | 13 +- .../CLUSTER_COUNT-FAILURE-REPORTS.spec.ts | 33 +- .../commands/CLUSTER_COUNT-FAILURE-REPORTS.ts | 13 +- .../commands/CLUSTER_COUNTKEYSINSLOT.spec.ts | 30 +- .../lib/commands/CLUSTER_COUNTKEYSINSLOT.ts | 13 +- .../lib/commands/CLUSTER_DELSLOTS.spec.ts | 30 +- .../client/lib/commands/CLUSTER_DELSLOTS.ts | 21 +- .../commands/CLUSTER_DELSLOTSRANGE.spec.ts | 48 +- .../lib/commands/CLUSTER_DELSLOTSRANGE.ts | 19 +- .../lib/commands/CLUSTER_FAILOVER.spec.ts | 34 +- .../client/lib/commands/CLUSTER_FAILOVER.ts | 29 +- .../lib/commands/CLUSTER_FLUSHSLOTS.spec.ts | 16 +- .../client/lib/commands/CLUSTER_FLUSHSLOTS.ts | 13 +- .../lib/commands/CLUSTER_FORGET.spec.ts | 16 +- .../client/lib/commands/CLUSTER_FORGET.ts | 13 +- .../commands/CLUSTER_GETKEYSINSLOT.spec.ts | 36 +- .../lib/commands/CLUSTER_GETKEYSINSLOT.ts | 13 +- .../client/lib/commands/CLUSTER_INFO.spec.ts | 65 +- packages/client/lib/commands/CLUSTER_INFO.ts | 55 +- .../lib/commands/CLUSTER_KEYSLOT.spec.ts | 30 +- .../client/lib/commands/CLUSTER_KEYSLOT.ts | 13 +- .../client/lib/commands/CLUSTER_LINKS.spec.ts | 44 +- packages/client/lib/commands/CLUSTER_LINKS.ts | 66 +- .../client/lib/commands/CLUSTER_MEET.spec.ts | 16 +- packages/client/lib/commands/CLUSTER_MEET.ts | 13 +- .../client/lib/commands/CLUSTER_MYID.spec.ts | 32 +- packages/client/lib/commands/CLUSTER_MYID.ts | 13 +- .../lib/commands/CLUSTER_MYSHARDID.spec.ts | 30 +- .../client/lib/commands/CLUSTER_MYSHARDID.ts | 12 +- .../client/lib/commands/CLUSTER_NODES.spec.ts | 157 +- packages/client/lib/commands/CLUSTER_NODES.ts | 113 +- .../lib/commands/CLUSTER_REPLICAS.spec.ts | 26 +- .../client/lib/commands/CLUSTER_REPLICAS.ts | 13 +- .../lib/commands/CLUSTER_REPLICATE.spec.ts | 16 +- .../client/lib/commands/CLUSTER_REPLICATE.ts | 13 +- .../client/lib/commands/CLUSTER_RESET.spec.ts | 32 +- packages/client/lib/commands/CLUSTER_RESET.ts | 21 +- .../lib/commands/CLUSTER_SAVECONFIG.spec.ts | 30 +- .../client/lib/commands/CLUSTER_SAVECONFIG.ts | 14 +- .../commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts | 16 +- .../lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts | 13 +- .../lib/commands/CLUSTER_SETSLOT.spec.ts | 30 +- .../client/lib/commands/CLUSTER_SETSLOT.ts | 35 +- .../client/lib/commands/CLUSTER_SLOTS.spec.ts | 98 +- packages/client/lib/commands/CLUSTER_SLOTS.ts | 73 +- packages/client/lib/commands/COMMAND.spec.ts | 30 +- packages/client/lib/commands/COMMAND.ts | 17 +- .../client/lib/commands/COMMAND_COUNT.spec.ts | 28 +- packages/client/lib/commands/COMMAND_COUNT.ts | 15 +- .../lib/commands/COMMAND_GETKEYS.spec.ts | 28 +- .../client/lib/commands/COMMAND_GETKEYS.ts | 15 +- .../commands/COMMAND_GETKEYSANDFLAGS.spec.ts | 42 +- .../lib/commands/COMMAND_GETKEYSANDFLAGS.ts | 37 +- .../client/lib/commands/COMMAND_INFO.spec.ts | 90 +- packages/client/lib/commands/COMMAND_INFO.ts | 18 +- .../client/lib/commands/COMMAND_LIST.spec.ts | 98 +- packages/client/lib/commands/COMMAND_LIST.ts | 50 +- .../client/lib/commands/CONFIG_GET.spec.ts | 34 +- packages/client/lib/commands/CONFIG_GET.ts | 17 +- .../lib/commands/CONFIG_RESETSTAT.spec.ts | 16 +- .../client/lib/commands/CONFIG_RESETSTAT.ts | 13 +- .../lib/commands/CONFIG_REWRITE.spec.ts | 16 +- .../client/lib/commands/CONFIG_REWRITE.ts | 13 +- .../client/lib/commands/CONFIG_SET.spec.ts | 46 +- packages/client/lib/commands/CONFIG_SET.ts | 35 +- packages/client/lib/commands/COPY.spec.ts | 103 +- packages/client/lib/commands/COPY.ts | 33 +- packages/client/lib/commands/DBSIZE.spec.ts | 28 +- packages/client/lib/commands/DBSIZE.ts | 13 +- packages/client/lib/commands/DECR.spec.ts | 31 +- packages/client/lib/commands/DECR.ts | 14 +- packages/client/lib/commands/DECRBY.spec.ts | 31 +- packages/client/lib/commands/DECRBY.ts | 17 +- packages/client/lib/commands/DEL.spec.ts | 45 +- packages/client/lib/commands/DEL.ts | 21 +- packages/client/lib/commands/DISCARD.spec.ts | 16 +- packages/client/lib/commands/DISCARD.ts | 11 +- packages/client/lib/commands/DUMP.spec.ts | 17 +- packages/client/lib/commands/DUMP.ts | 15 +- packages/client/lib/commands/ECHO.spec.ts | 28 +- packages/client/lib/commands/ECHO.ts | 15 +- packages/client/lib/commands/EVAL.spec.ts | 44 +- packages/client/lib/commands/EVAL.ts | 34 +- packages/client/lib/commands/EVALSHA.spec.ts | 22 +- packages/client/lib/commands/EVALSHA.ts | 14 +- .../client/lib/commands/EVALSHA_RO.spec.ts | 24 +- packages/client/lib/commands/EVALSHA_RO.ts | 16 +- packages/client/lib/commands/EVAL_RO.spec.ts | 46 +- packages/client/lib/commands/EVAL_RO.ts | 16 +- packages/client/lib/commands/EXISTS.spec.ts | 45 +- packages/client/lib/commands/EXISTS.ts | 25 +- packages/client/lib/commands/EXPIRE.spec.ts | 45 +- packages/client/lib/commands/EXPIRE.ts | 21 +- packages/client/lib/commands/EXPIREAT.spec.ts | 61 +- packages/client/lib/commands/EXPIREAT.ts | 27 +- .../client/lib/commands/EXPIRETIME.spec.ts | 33 +- packages/client/lib/commands/EXPIRETIME.ts | 15 +- packages/client/lib/commands/FAILOVER.spec.ts | 126 +- packages/client/lib/commands/FAILOVER.ts | 37 +- packages/client/lib/commands/FCALL.spec.ts | 45 +- packages/client/lib/commands/FCALL.ts | 14 +- packages/client/lib/commands/FCALL_RO.spec.ts | 45 +- packages/client/lib/commands/FCALL_RO.ts | 16 +- packages/client/lib/commands/FLUSHALL.spec.ts | 54 +- packages/client/lib/commands/FLUSHALL.ts | 27 +- packages/client/lib/commands/FLUSHDB.spec.ts | 56 +- packages/client/lib/commands/FLUSHDB.ts | 18 +- .../lib/commands/FUNCTION_DELETE.spec.ts | 34 +- .../client/lib/commands/FUNCTION_DELETE.ts | 13 +- .../client/lib/commands/FUNCTION_DUMP.spec.ts | 30 +- packages/client/lib/commands/FUNCTION_DUMP.ts | 13 +- .../lib/commands/FUNCTION_FLUSH.spec.ts | 44 +- .../client/lib/commands/FUNCTION_FLUSH.ts | 18 +- .../client/lib/commands/FUNCTION_KILL.spec.ts | 18 +- packages/client/lib/commands/FUNCTION_KILL.ts | 13 +- .../client/lib/commands/FUNCTION_LIST.spec.ts | 68 +- packages/client/lib/commands/FUNCTION_LIST.ts | 57 +- .../commands/FUNCTION_LIST_WITHCODE.spec.ts | 72 +- .../lib/commands/FUNCTION_LIST_WITHCODE.ts | 60 +- .../client/lib/commands/FUNCTION_LOAD.spec.ts | 101 +- packages/client/lib/commands/FUNCTION_LOAD.ts | 24 +- .../lib/commands/FUNCTION_RESTORE.spec.ts | 61 +- .../client/lib/commands/FUNCTION_RESTORE.ts | 24 +- .../lib/commands/FUNCTION_STATS.spec.ts | 38 +- .../client/lib/commands/FUNCTION_STATS.ts | 112 +- packages/client/lib/commands/GEOADD.spec.ts | 173 +- packages/client/lib/commands/GEOADD.ts | 84 +- packages/client/lib/commands/GEODIST.spec.ts | 93 +- packages/client/lib/commands/GEODIST.ts | 31 +- packages/client/lib/commands/GEOHASH.spec.ts | 52 +- packages/client/lib/commands/GEOHASH.ts | 29 +- packages/client/lib/commands/GEOPOS.spec.ts | 106 +- packages/client/lib/commands/GEOPOS.ts | 45 +- .../client/lib/commands/GEORADIUS.spec.ts | 53 +- packages/client/lib/commands/GEORADIUS.ts | 47 +- .../lib/commands/GEORADIUSBYMEMBER.spec.ts | 38 +- .../client/lib/commands/GEORADIUSBYMEMBER.ts | 45 +- .../commands/GEORADIUSBYMEMBERSTORE.spec.ts | 53 - .../lib/commands/GEORADIUSBYMEMBERSTORE.ts | 25 - .../lib/commands/GEORADIUSBYMEMBER_RO.spec.ts | 38 +- .../lib/commands/GEORADIUSBYMEMBER_RO.ts | 34 +- .../GEORADIUSBYMEMBER_RO_WITH.spec.ts | 61 +- .../lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts | 39 +- .../commands/GEORADIUSBYMEMBER_STORE.spec.ts | 39 + .../lib/commands/GEORADIUSBYMEMBER_STORE.ts | 31 + .../commands/GEORADIUSBYMEMBER_WITH.spec.ts | 61 +- .../lib/commands/GEORADIUSBYMEMBER_WITH.ts | 54 +- .../lib/commands/GEORADIUSSTORE.spec.ts | 53 - .../client/lib/commands/GEORADIUSSTORE.ts | 25 - .../client/lib/commands/GEORADIUS_RO.spec.ts | 53 +- packages/client/lib/commands/GEORADIUS_RO.ts | 34 +- .../lib/commands/GEORADIUS_RO_WITH.spec.ts | 74 +- .../client/lib/commands/GEORADIUS_RO_WITH.ts | 39 +- .../lib/commands/GEORADIUS_STORE.spec.ts | 48 + .../client/lib/commands/GEORADIUS_STORE.ts | 31 + .../lib/commands/GEORADIUS_WITH.spec.ts | 74 +- .../client/lib/commands/GEORADIUS_WITH.ts | 57 +- .../client/lib/commands/GEOSEARCH.spec.ts | 100 +- packages/client/lib/commands/GEOSEARCH.ts | 97 +- .../lib/commands/GEOSEARCHSTORE.spec.ts | 111 +- .../client/lib/commands/GEOSEARCHSTORE.ts | 43 +- .../lib/commands/GEOSEARCH_WITH.spec.ts | 75 +- .../client/lib/commands/GEOSEARCH_WITH.ts | 76 +- packages/client/lib/commands/GET.spec.ts | 45 +- packages/client/lib/commands/GET.ts | 17 +- packages/client/lib/commands/GETBIT.spec.ts | 38 +- packages/client/lib/commands/GETBIT.ts | 20 +- packages/client/lib/commands/GETDEL.spec.ts | 40 +- packages/client/lib/commands/GETDEL.ts | 15 +- packages/client/lib/commands/GETEX.spec.ts | 186 +- packages/client/lib/commands/GETEX.ts | 91 +- packages/client/lib/commands/GETRANGE.spec.ts | 38 +- packages/client/lib/commands/GETRANGE.ts | 21 +- packages/client/lib/commands/GETSET.spec.ts | 38 +- packages/client/lib/commands/GETSET.ts | 18 +- packages/client/lib/commands/HDEL.spec.ts | 45 +- packages/client/lib/commands/HDEL.ts | 21 +- packages/client/lib/commands/HELLO.spec.ts | 125 +- packages/client/lib/commands/HELLO.ts | 116 +- packages/client/lib/commands/HEXISTS.spec.ts | 31 +- packages/client/lib/commands/HEXISTS.ts | 18 +- packages/client/lib/commands/HEXPIRE.spec.ts | 8 +- packages/client/lib/commands/HEXPIRE.ts | 45 +- .../client/lib/commands/HEXPIREAT.spec.ts | 10 +- packages/client/lib/commands/HEXPIREAT.ts | 52 +- .../client/lib/commands/HEXPIRETIME.spec.ts | 6 +- packages/client/lib/commands/HEXPIRETIME.ts | 23 +- packages/client/lib/commands/HGET.spec.ts | 31 +- packages/client/lib/commands/HGET.ts | 20 +- packages/client/lib/commands/HGETALL.spec.ts | 63 +- packages/client/lib/commands/HGETALL.ts | 24 +- packages/client/lib/commands/HINCRBY.spec.ts | 31 +- packages/client/lib/commands/HINCRBY.ts | 27 +- .../client/lib/commands/HINCRBYFLOAT.spec.ts | 31 +- packages/client/lib/commands/HINCRBYFLOAT.ts | 27 +- packages/client/lib/commands/HKEYS.spec.ts | 31 +- packages/client/lib/commands/HKEYS.ts | 15 +- packages/client/lib/commands/HLEN.spec.ts | 31 +- packages/client/lib/commands/HLEN.ts | 15 +- packages/client/lib/commands/HMGET.spec.ts | 45 +- packages/client/lib/commands/HMGET.ts | 29 +- packages/client/lib/commands/HPERSIST.spec.ts | 6 +- packages/client/lib/commands/HPERSIST.ts | 19 +- packages/client/lib/commands/HPEXPIRE.spec.ts | 8 +- packages/client/lib/commands/HPEXPIRE.ts | 44 +- .../client/lib/commands/HPEXPIREAT.spec.ts | 10 +- packages/client/lib/commands/HPEXPIREAT.ts | 39 +- .../client/lib/commands/HPEXPIRETIME.spec.ts | 6 +- packages/client/lib/commands/HPEXPIRETIME.ts | 20 +- packages/client/lib/commands/HPTTL.spec.ts | 6 +- packages/client/lib/commands/HPTTL.ts | 20 +- .../client/lib/commands/HRANDFIELD.spec.ts | 33 +- packages/client/lib/commands/HRANDFIELD.ts | 17 +- .../lib/commands/HRANDFIELD_COUNT.spec.ts | 33 +- .../client/lib/commands/HRANDFIELD_COUNT.ts | 24 +- .../HRANDFIELD_COUNT_WITHVALUES.spec.ts | 21 - .../commands/HRANDFIELD_COUNT_WITHVALUES.ts | 49 +- packages/client/lib/commands/HSCAN.spec.ts | 148 +- packages/client/lib/commands/HSCAN.ts | 64 +- .../lib/commands/HSCAN_NOVALUES.spec.ts | 135 +- .../client/lib/commands/HSCAN_NOVALUES.ts | 39 +- packages/client/lib/commands/HSET.spec.ts | 118 +- packages/client/lib/commands/HSET.ts | 98 +- packages/client/lib/commands/HSETNX.spec.ts | 31 +- packages/client/lib/commands/HSETNX.ts | 23 +- packages/client/lib/commands/HSTRLEN.spec.ts | 31 +- packages/client/lib/commands/HSTRLEN.ts | 18 +- packages/client/lib/commands/HTTL.spec.ts | 6 +- packages/client/lib/commands/HTTL.ts | 20 +- packages/client/lib/commands/HVALS.spec.ts | 31 +- packages/client/lib/commands/HVALS.ts | 15 +- packages/client/lib/commands/INCR.spec.ts | 31 +- packages/client/lib/commands/INCR.ts | 14 +- packages/client/lib/commands/INCRBY.spec.ts | 33 +- packages/client/lib/commands/INCRBY.ts | 17 +- .../client/lib/commands/INCRBYFLOAT.spec.ts | 31 +- packages/client/lib/commands/INCRBYFLOAT.ts | 17 +- packages/client/lib/commands/INFO.spec.ts | 38 +- packages/client/lib/commands/INFO.ts | 17 +- packages/client/lib/commands/KEYS.spec.ts | 14 +- packages/client/lib/commands/KEYS.ts | 13 +- packages/client/lib/commands/LASTSAVE.spec.ts | 25 +- packages/client/lib/commands/LASTSAVE.ts | 15 +- .../lib/commands/LATENCY_DOCTOR.spec.ts | 30 +- .../client/lib/commands/LATENCY_DOCTOR.ts | 13 +- .../client/lib/commands/LATENCY_GRAPH.spec.ts | 42 +- packages/client/lib/commands/LATENCY_GRAPH.ts | 50 +- .../lib/commands/LATENCY_HISTORY.spec.ts | 44 +- .../client/lib/commands/LATENCY_HISTORY.ts | 52 +- .../lib/commands/LATENCY_LATEST.spec.ts | 46 +- .../client/lib/commands/LATENCY_LATEST.ts | 22 +- packages/client/lib/commands/LCS.spec.ts | 40 +- packages/client/lib/commands/LCS.ts | 31 +- packages/client/lib/commands/LCS_IDX.spec.ts | 63 +- packages/client/lib/commands/LCS_IDX.ts | 82 +- .../lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts | 64 +- .../lib/commands/LCS_IDX_WITHMATCHLEN.ts | 75 +- packages/client/lib/commands/LCS_LEN.spec.ts | 40 +- packages/client/lib/commands/LCS_LEN.ts | 25 +- packages/client/lib/commands/LINDEX.spec.ts | 50 +- packages/client/lib/commands/LINDEX.ts | 20 +- packages/client/lib/commands/LINSERT.spec.ts | 38 +- packages/client/lib/commands/LINSERT.ts | 33 +- packages/client/lib/commands/LLEN.spec.ts | 38 +- packages/client/lib/commands/LLEN.ts | 17 +- packages/client/lib/commands/LMOVE.spec.ts | 40 +- packages/client/lib/commands/LMOVE.ts | 31 +- packages/client/lib/commands/LMPOP.spec.ts | 66 +- packages/client/lib/commands/LMPOP.ts | 51 +- packages/client/lib/commands/LOLWUT.spec.ts | 54 +- packages/client/lib/commands/LOLWUT.ts | 25 +- packages/client/lib/commands/LPOP.spec.ts | 38 +- packages/client/lib/commands/LPOP.ts | 14 +- .../client/lib/commands/LPOP_COUNT.spec.ts | 40 +- packages/client/lib/commands/LPOP_COUNT.ts | 18 +- packages/client/lib/commands/LPOS.spec.ts | 92 +- packages/client/lib/commands/LPOS.ts | 35 +- .../client/lib/commands/LPOS_COUNT.spec.ts | 92 +- packages/client/lib/commands/LPOS_COUNT.ts | 31 +- packages/client/lib/commands/LPUSH.spec.ts | 52 +- packages/client/lib/commands/LPUSH.ts | 20 +- packages/client/lib/commands/LPUSHX.spec.ts | 52 +- packages/client/lib/commands/LPUSHX.ts | 21 +- packages/client/lib/commands/LRANGE.spec.ts | 39 +- packages/client/lib/commands/LRANGE.ts | 29 +- packages/client/lib/commands/LREM.spec.ts | 39 +- packages/client/lib/commands/LREM.ts | 29 +- packages/client/lib/commands/LSET.spec.ts | 41 +- packages/client/lib/commands/LSET.ts | 29 +- packages/client/lib/commands/LTRIM.spec.ts | 38 +- packages/client/lib/commands/LTRIM.ts | 26 +- .../client/lib/commands/MEMORY_DOCTOR.spec.ts | 28 +- packages/client/lib/commands/MEMORY_DOCTOR.ts | 13 +- .../lib/commands/MEMORY_MALLOC-STATS.spec.ts | 28 +- .../lib/commands/MEMORY_MALLOC-STATS.ts | 12 +- .../client/lib/commands/MEMORY_PURGE.spec.ts | 28 +- packages/client/lib/commands/MEMORY_PURGE.ts | 12 +- .../client/lib/commands/MEMORY_STATS.spec.ts | 146 +- packages/client/lib/commands/MEMORY_STATS.ts | 151 +- .../client/lib/commands/MEMORY_USAGE.spec.ts | 46 +- packages/client/lib/commands/MEMORY_USAGE.ts | 23 +- packages/client/lib/commands/MGET.spec.ts | 38 +- packages/client/lib/commands/MGET.ts | 19 +- packages/client/lib/commands/MIGRATE.spec.ts | 134 +- packages/client/lib/commands/MIGRATE.ts | 86 +- .../client/lib/commands/MODULE_LIST.spec.ts | 16 +- packages/client/lib/commands/MODULE_LIST.ts | 29 +- .../client/lib/commands/MODULE_LOAD.spec.ts | 30 +- packages/client/lib/commands/MODULE_LOAD.ts | 19 +- .../client/lib/commands/MODULE_UNLOAD.spec.ts | 16 +- packages/client/lib/commands/MODULE_UNLOAD.ts | 13 +- packages/client/lib/commands/MOVE.spec.ts | 28 +- packages/client/lib/commands/MOVE.ts | 12 +- packages/client/lib/commands/MSET.spec.ts | 64 +- packages/client/lib/commands/MSET.ts | 35 +- packages/client/lib/commands/MSETNX.spec.ts | 64 +- packages/client/lib/commands/MSETNX.ts | 29 +- .../lib/commands/OBJECT_ENCODING.spec.ts | 31 +- .../client/lib/commands/OBJECT_ENCODING.ts | 17 +- .../client/lib/commands/OBJECT_FREQ.spec.ts | 31 +- packages/client/lib/commands/OBJECT_FREQ.ts | 17 +- .../lib/commands/OBJECT_IDLETIME.spec.ts | 31 +- .../client/lib/commands/OBJECT_IDLETIME.ts | 17 +- .../lib/commands/OBJECT_REFCOUNT.spec.ts | 31 +- .../client/lib/commands/OBJECT_REFCOUNT.ts | 17 +- packages/client/lib/commands/PERSIST.spec.ts | 31 +- packages/client/lib/commands/PERSIST.ts | 14 +- packages/client/lib/commands/PEXPIRE.spec.ts | 45 +- packages/client/lib/commands/PEXPIRE.ts | 25 +- .../client/lib/commands/PEXPIREAT.spec.ts | 59 +- packages/client/lib/commands/PEXPIREAT.ts | 29 +- .../client/lib/commands/PEXPIRETIME.spec.ts | 33 +- packages/client/lib/commands/PEXPIRETIME.ts | 15 +- packages/client/lib/commands/PFADD.spec.ts | 45 +- packages/client/lib/commands/PFADD.ts | 23 +- packages/client/lib/commands/PFCOUNT.spec.ts | 45 +- packages/client/lib/commands/PFCOUNT.ts | 21 +- packages/client/lib/commands/PFMERGE.spec.ts | 45 +- packages/client/lib/commands/PFMERGE.ts | 22 +- packages/client/lib/commands/PING.spec.ts | 54 +- packages/client/lib/commands/PING.ts | 17 +- packages/client/lib/commands/PSETEX.spec.ts | 39 +- packages/client/lib/commands/PSETEX.ts | 30 +- packages/client/lib/commands/PTTL.spec.ts | 31 +- packages/client/lib/commands/PTTL.ts | 17 +- packages/client/lib/commands/PUBLISH.spec.ts | 28 +- packages/client/lib/commands/PUBLISH.ts | 19 +- .../lib/commands/PUBSUB_CHANNELS.spec.ts | 42 +- .../client/lib/commands/PUBSUB_CHANNELS.ts | 16 +- .../client/lib/commands/PUBSUB_NUMPAT.spec.ts | 28 +- packages/client/lib/commands/PUBSUB_NUMPAT.ts | 13 +- .../client/lib/commands/PUBSUB_NUMSUB.spec.ts | 54 +- packages/client/lib/commands/PUBSUB_NUMSUB.ts | 33 +- .../lib/commands/PUBSUB_SHARDCHANNELS.spec.ts | 46 +- .../lib/commands/PUBSUB_SHARDCHANNELS.ts | 23 +- .../lib/commands/PUBSUB_SHARDNUMSUB.spec.ts | 76 +- .../client/lib/commands/PUBSUB_SHARDNUMSUB.ts | 32 +- .../client/lib/commands/RANDOMKEY.spec.ts | 31 +- packages/client/lib/commands/RANDOMKEY.ts | 15 +- packages/client/lib/commands/READONLY.spec.ts | 16 +- packages/client/lib/commands/READONLY.ts | 13 +- .../client/lib/commands/READWRITE.spec.ts | 16 +- packages/client/lib/commands/READWRITE.ts | 13 +- packages/client/lib/commands/RENAME.spec.ts | 35 +- packages/client/lib/commands/RENAME.ts | 18 +- packages/client/lib/commands/RENAMENX.spec.ts | 33 +- packages/client/lib/commands/RENAMENX.ts | 18 +- .../client/lib/commands/REPLICAOF.spec.ts | 16 +- packages/client/lib/commands/REPLICAOF.ts | 13 +- .../lib/commands/RESTORE-ASKING.spec.ts | 16 +- .../client/lib/commands/RESTORE-ASKING.ts | 13 +- packages/client/lib/commands/RESTORE.spec.ts | 132 +- packages/client/lib/commands/RESTORE.ts | 39 +- packages/client/lib/commands/ROLE.spec.ts | 120 +- packages/client/lib/commands/ROLE.ts | 125 +- packages/client/lib/commands/RPOP.spec.ts | 38 +- packages/client/lib/commands/RPOP.ts | 14 +- .../client/lib/commands/RPOPLPUSH.spec.ts | 38 +- packages/client/lib/commands/RPOPLPUSH.ts | 20 +- .../client/lib/commands/RPOP_COUNT.spec.ts | 40 +- packages/client/lib/commands/RPOP_COUNT.ts | 17 +- packages/client/lib/commands/RPUSH.spec.ts | 52 +- packages/client/lib/commands/RPUSH.ts | 24 +- packages/client/lib/commands/RPUSHX.spec.ts | 52 +- packages/client/lib/commands/RPUSHX.ts | 24 +- packages/client/lib/commands/SADD.spec.ts | 45 +- packages/client/lib/commands/SADD.ts | 21 +- packages/client/lib/commands/SAVE.spec.ts | 16 +- packages/client/lib/commands/SAVE.ts | 13 +- packages/client/lib/commands/SCAN.spec.ts | 126 +- packages/client/lib/commands/SCAN.ts | 60 +- packages/client/lib/commands/SCARD.spec.ts | 31 +- packages/client/lib/commands/SCARD.ts | 13 +- .../client/lib/commands/SCRIPT_DEBUG.spec.ts | 28 +- packages/client/lib/commands/SCRIPT_DEBUG.ts | 13 +- .../client/lib/commands/SCRIPT_EXISTS.spec.ts | 42 +- packages/client/lib/commands/SCRIPT_EXISTS.ts | 17 +- .../client/lib/commands/SCRIPT_FLUSH.spec.ts | 42 +- packages/client/lib/commands/SCRIPT_FLUSH.ts | 15 +- .../client/lib/commands/SCRIPT_KILL.spec.ts | 16 +- packages/client/lib/commands/SCRIPT_KILL.ts | 13 +- .../client/lib/commands/SCRIPT_LOAD.spec.ts | 34 +- packages/client/lib/commands/SCRIPT_LOAD.ts | 13 +- packages/client/lib/commands/SDIFF.spec.ts | 45 +- packages/client/lib/commands/SDIFF.ts | 25 +- .../client/lib/commands/SDIFFSTORE.spec.ts | 45 +- packages/client/lib/commands/SDIFFSTORE.ts | 21 +- packages/client/lib/commands/SET.spec.ts | 257 +- packages/client/lib/commands/SET.ts | 114 +- packages/client/lib/commands/SETBIT.spec.ts | 38 +- packages/client/lib/commands/SETBIT.ts | 19 +- packages/client/lib/commands/SETEX.spec.ts | 38 +- packages/client/lib/commands/SETEX.ts | 28 +- packages/client/lib/commands/SETNX .spec.ts | 38 +- packages/client/lib/commands/SETNX.ts | 17 +- packages/client/lib/commands/SETRANGE.spec.ts | 38 +- packages/client/lib/commands/SETRANGE.ts | 27 +- packages/client/lib/commands/SHUTDOWN.spec.ts | 64 +- packages/client/lib/commands/SHUTDOWN.ts | 38 +- packages/client/lib/commands/SINTER.spec.ts | 45 +- packages/client/lib/commands/SINTER.ts | 25 +- .../client/lib/commands/SINTERCARD.spec.ts | 56 +- packages/client/lib/commands/SINTERCARD.ts | 35 +- .../client/lib/commands/SINTERSTORE.spec.ts | 45 +- packages/client/lib/commands/SINTERSTORE.ts | 25 +- .../client/lib/commands/SISMEMBER.spec.ts | 31 +- packages/client/lib/commands/SISMEMBER.ts | 18 +- packages/client/lib/commands/SMEMBERS.spec.ts | 31 +- packages/client/lib/commands/SMEMBERS.ts | 18 +- .../client/lib/commands/SMISMEMBER.spec.ts | 33 +- packages/client/lib/commands/SMISMEMBER.ts | 18 +- packages/client/lib/commands/SMOVE.spec.ts | 31 +- packages/client/lib/commands/SMOVE.ts | 23 +- packages/client/lib/commands/SORT.spec.ts | 169 +- packages/client/lib/commands/SORT.ts | 64 +- packages/client/lib/commands/SORT_RO.spec.ts | 171 +- packages/client/lib/commands/SORT_RO.ts | 24 +- .../client/lib/commands/SORT_STORE.spec.ts | 169 +- packages/client/lib/commands/SORT_STORE.ts | 26 +- packages/client/lib/commands/SPOP.spec.ts | 40 +- packages/client/lib/commands/SPOP.ts | 28 +- .../client/lib/commands/SPOP_COUNT.spec.ts | 22 + packages/client/lib/commands/SPOP_COUNT.ts | 10 + packages/client/lib/commands/SPUBLISH.spec.ts | 35 +- packages/client/lib/commands/SPUBLISH.ts | 20 +- .../client/lib/commands/SRANDMEMBER.spec.ts | 31 +- packages/client/lib/commands/SRANDMEMBER.ts | 15 +- .../lib/commands/SRANDMEMBER_COUNT.spec.ts | 31 +- .../client/lib/commands/SRANDMEMBER_COUNT.ts | 27 +- packages/client/lib/commands/SREM.spec.ts | 45 +- packages/client/lib/commands/SREM.ts | 22 +- packages/client/lib/commands/SSCAN.spec.ts | 109 +- packages/client/lib/commands/SSCAN.ts | 45 +- packages/client/lib/commands/STRLEN.spec.ts | 38 +- packages/client/lib/commands/STRLEN.ts | 17 +- packages/client/lib/commands/SUNION.spec.ts | 45 +- packages/client/lib/commands/SUNION.ts | 25 +- .../client/lib/commands/SUNIONSTORE.spec.ts | 45 +- packages/client/lib/commands/SUNIONSTORE.ts | 25 +- packages/client/lib/commands/SWAPDB.spec.ts | 28 +- packages/client/lib/commands/SWAPDB.ts | 12 +- packages/client/lib/commands/TIME.spec.ts | 27 +- packages/client/lib/commands/TIME.ts | 26 +- packages/client/lib/commands/TOUCH.spec.ts | 45 +- packages/client/lib/commands/TOUCH.ts | 21 +- packages/client/lib/commands/TTL.spec.ts | 31 +- packages/client/lib/commands/TTL.ts | 17 +- packages/client/lib/commands/TYPE.spec.ts | 31 +- packages/client/lib/commands/TYPE.ts | 17 +- packages/client/lib/commands/UNLINK.spec.ts | 45 +- packages/client/lib/commands/UNLINK.ts | 21 +- packages/client/lib/commands/UNWATCH.spec.ts | 19 - packages/client/lib/commands/UNWATCH.ts | 5 - packages/client/lib/commands/WAIT.spec.ts | 28 +- packages/client/lib/commands/WAIT.ts | 13 +- packages/client/lib/commands/WATCH.spec.ts | 20 - packages/client/lib/commands/WATCH.ts | 10 - packages/client/lib/commands/XACK.spec.ts | 45 +- packages/client/lib/commands/XACK.ts | 27 +- packages/client/lib/commands/XADD.spec.ts | 189 +- packages/client/lib/commands/XADD.ts | 83 +- .../lib/commands/XADD_NOMKSTREAM.spec.ts | 95 + .../client/lib/commands/XADD_NOMKSTREAM.ts | 16 + .../client/lib/commands/XAUTOCLAIM.spec.ts | 146 +- packages/client/lib/commands/XAUTOCLAIM.ts | 60 +- .../lib/commands/XAUTOCLAIM_JUSTID.spec.ts | 54 +- .../client/lib/commands/XAUTOCLAIM_JUSTID.ts | 42 +- packages/client/lib/commands/XCLAIM.spec.ts | 217 +- packages/client/lib/commands/XCLAIM.ts | 72 +- .../client/lib/commands/XCLAIM_JUSTID.spec.ts | 35 +- packages/client/lib/commands/XCLAIM_JUSTID.ts | 24 +- packages/client/lib/commands/XDEL.spec.ts | 45 +- packages/client/lib/commands/XDEL.ts | 22 +- .../client/lib/commands/XGROUP_CREATE.spec.ts | 62 +- packages/client/lib/commands/XGROUP_CREATE.ts | 36 +- .../commands/XGROUP_CREATECONSUMER.spec.ts | 39 +- .../lib/commands/XGROUP_CREATECONSUMER.ts | 23 +- .../lib/commands/XGROUP_DELCONSUMER.spec.ts | 37 +- .../client/lib/commands/XGROUP_DELCONSUMER.ts | 23 +- .../lib/commands/XGROUP_DESTROY.spec.ts | 37 +- .../client/lib/commands/XGROUP_DESTROY.ts | 21 +- .../client/lib/commands/XGROUP_SETID.spec.ts | 37 +- packages/client/lib/commands/XGROUP_SETID.ts | 33 +- .../lib/commands/XINFO_CONSUMERS.spec.ts | 69 +- .../client/lib/commands/XINFO_CONSUMERS.ts | 56 +- .../client/lib/commands/XINFO_GROUPS.spec.ts | 74 +- packages/client/lib/commands/XINFO_GROUPS.ts | 55 +- .../client/lib/commands/XINFO_STREAM.spec.ts | 99 +- packages/client/lib/commands/XINFO_STREAM.ts | 124 +- packages/client/lib/commands/XLEN.spec.ts | 31 +- packages/client/lib/commands/XLEN.ts | 17 +- packages/client/lib/commands/XPENDING.spec.ts | 102 +- packages/client/lib/commands/XPENDING.ts | 70 +- .../lib/commands/XPENDING_RANGE.spec.ts | 99 +- .../client/lib/commands/XPENDING_RANGE.ts | 83 +- packages/client/lib/commands/XRANGE.spec.ts | 61 +- packages/client/lib/commands/XRANGE.ts | 47 +- packages/client/lib/commands/XREAD.spec.ts | 209 +- packages/client/lib/commands/XREAD.ts | 72 +- .../client/lib/commands/XREADGROUP.spec.ts | 270 +- packages/client/lib/commands/XREADGROUP.ts | 74 +- .../client/lib/commands/XREVRANGE.spec.ts | 61 +- packages/client/lib/commands/XREVRANGE.ts | 33 +- packages/client/lib/commands/XSETID.spec.ts | 46 + packages/client/lib/commands/XSETID.ts | 33 +- packages/client/lib/commands/XTRIM.spec.ts | 83 +- packages/client/lib/commands/XTRIM.ts | 30 +- packages/client/lib/commands/ZADD.spec.ts | 244 +- packages/client/lib/commands/ZADD.ts | 127 +- .../client/lib/commands/ZADD_INCR.spec.ts | 93 + packages/client/lib/commands/ZADD_INCR.ts | 39 + packages/client/lib/commands/ZCARD.spec.ts | 31 +- packages/client/lib/commands/ZCARD.ts | 17 +- packages/client/lib/commands/ZCOUNT.spec.ts | 31 +- packages/client/lib/commands/ZCOUNT.ts | 35 +- packages/client/lib/commands/ZDIFF.spec.ts | 47 +- packages/client/lib/commands/ZDIFF.ts | 25 +- .../client/lib/commands/ZDIFFSTORE.spec.ts | 47 +- packages/client/lib/commands/ZDIFFSTORE.ts | 25 +- .../lib/commands/ZDIFF_WITHSCORES.spec.ts | 47 +- .../client/lib/commands/ZDIFF_WITHSCORES.ts | 25 +- packages/client/lib/commands/ZINCRBY.spec.ts | 31 +- packages/client/lib/commands/ZINCRBY.ts | 30 +- packages/client/lib/commands/ZINTER.spec.ts | 101 +- packages/client/lib/commands/ZINTER.ts | 54 +- .../client/lib/commands/ZINTERCARD.spec.ts | 56 +- packages/client/lib/commands/ZINTERCARD.ts | 36 +- .../client/lib/commands/ZINTERSTORE.spec.ts | 99 +- packages/client/lib/commands/ZINTERSTORE.ts | 45 +- .../lib/commands/ZINTER_WITHSCORES.spec.ts | 101 +- .../client/lib/commands/ZINTER_WITHSCORES.ts | 25 +- .../client/lib/commands/ZLEXCOUNT.spec.ts | 31 +- packages/client/lib/commands/ZLEXCOUNT.ts | 33 +- packages/client/lib/commands/ZMPOP.spec.ts | 71 +- packages/client/lib/commands/ZMPOP.ts | 85 +- packages/client/lib/commands/ZMSCORE.spec.ts | 47 +- packages/client/lib/commands/ZMSCORE.ts | 32 +- packages/client/lib/commands/ZPOPMAX.spec.ts | 64 +- packages/client/lib/commands/ZPOPMAX.ts | 34 +- .../client/lib/commands/ZPOPMAX_COUNT.spec.ts | 41 +- packages/client/lib/commands/ZPOPMAX_COUNT.ts | 25 +- packages/client/lib/commands/ZPOPMIN.spec.ts | 64 +- packages/client/lib/commands/ZPOPMIN.ts | 21 +- .../client/lib/commands/ZPOPMIN_COUNT.spec.ts | 41 +- packages/client/lib/commands/ZPOPMIN_COUNT.ts | 25 +- .../client/lib/commands/ZRANDMEMBER.spec.ts | 33 +- packages/client/lib/commands/ZRANDMEMBER.ts | 17 +- .../lib/commands/ZRANDMEMBER_COUNT.spec.ts | 33 +- .../client/lib/commands/ZRANDMEMBER_COUNT.ts | 27 +- .../ZRANDMEMBER_COUNT_WITHSCORES.spec.ts | 33 +- .../commands/ZRANDMEMBER_COUNT_WITHSCORES.ts | 25 +- packages/client/lib/commands/ZRANGE.spec.ts | 129 +- packages/client/lib/commands/ZRANGE.ts | 71 +- .../client/lib/commands/ZRANGEBYLEX.spec.ts | 55 +- packages/client/lib/commands/ZRANGEBYLEX.ts | 45 +- .../client/lib/commands/ZRANGEBYSCORE.spec.ts | 55 +- packages/client/lib/commands/ZRANGEBYSCORE.ts | 43 +- .../commands/ZRANGEBYSCORE_WITHSCORES.spec.ts | 55 +- .../lib/commands/ZRANGEBYSCORE_WITHSCORES.ts | 30 +- .../client/lib/commands/ZRANGESTORE.spec.ts | 145 +- packages/client/lib/commands/ZRANGESTORE.ts | 80 +- .../lib/commands/ZRANGE_WITHSCORES.spec.ts | 120 +- .../client/lib/commands/ZRANGE_WITHSCORES.ts | 24 +- packages/client/lib/commands/ZRANK.spec.ts | 31 +- packages/client/lib/commands/ZRANK.ts | 20 +- .../lib/commands/ZRANK_WITHSCORE.spec.ts | 46 + .../client/lib/commands/ZRANK_WITHSCORE.ts | 30 + packages/client/lib/commands/ZREM.spec.ts | 45 +- packages/client/lib/commands/ZREM.ts | 25 +- .../lib/commands/ZREMRANGEBYLEX.spec.ts | 31 +- .../client/lib/commands/ZREMRANGEBYLEX.ts | 33 +- .../lib/commands/ZREMRANGEBYRANK.spec.ts | 31 +- .../client/lib/commands/ZREMRANGEBYRANK.ts | 20 +- .../lib/commands/ZREMRANGEBYSCORE.spec.ts | 31 +- .../client/lib/commands/ZREMRANGEBYSCORE.ts | 33 +- packages/client/lib/commands/ZREVRANK.spec.ts | 31 +- packages/client/lib/commands/ZREVRANK.ts | 20 +- packages/client/lib/commands/ZSCAN.spec.ts | 109 +- packages/client/lib/commands/ZSCAN.ts | 55 +- packages/client/lib/commands/ZSCORE.spec.ts | 31 +- packages/client/lib/commands/ZSCORE.ts | 20 +- packages/client/lib/commands/ZUNION.spec.ts | 93 +- packages/client/lib/commands/ZUNION.ts | 36 +- .../client/lib/commands/ZUNIONSTORE.spec.ts | 99 +- packages/client/lib/commands/ZUNIONSTORE.ts | 36 +- .../lib/commands/ZUNION_WITHSCORES.spec.ts | 93 +- .../client/lib/commands/ZUNION_WITHSCORES.ts | 25 +- .../lib/commands/generic-transformers.spec.ts | 1403 +++-- .../lib/commands/generic-transformers.ts | 1076 ++-- packages/client/lib/commands/index.ts | 1121 +++- packages/client/lib/errors.ts | 100 +- packages/client/lib/lua-script.ts | 24 +- packages/client/lib/multi-command.spec.ts | 139 +- packages/client/lib/multi-command.ts | 125 +- .../lib/sentinel/commands/SENTINEL_MASTER.ts | 12 + .../lib/sentinel/commands/SENTINEL_MONITOR.ts | 8 + .../sentinel/commands/SENTINEL_REPLICAS.ts | 23 + .../sentinel/commands/SENTINEL_SENTINELS.ts | 23 + .../lib/sentinel/commands/SENTINEL_SET.ts | 19 + .../client/lib/sentinel/commands/index.ts | 19 + packages/client/lib/sentinel/index.spec.ts | 1276 +++++ packages/client/lib/sentinel/index.ts | 1487 ++++++ packages/client/lib/sentinel/module.ts | 7 + .../client/lib/sentinel/multi-commands.ts | 219 + packages/client/lib/sentinel/pub-sub-proxy.ts | 209 + packages/client/lib/sentinel/test-util.ts | 605 +++ packages/client/lib/sentinel/types.ts | 175 + packages/client/lib/sentinel/utils.ts | 114 + packages/client/lib/sentinel/wait-queue.ts | 24 + packages/client/lib/test-utils.ts | 86 +- packages/client/lib/utils.ts | 3 - packages/client/package.json | 32 +- packages/client/tsconfig.json | 9 +- packages/graph/.release-it.json | 1 + .../graph/lib/commands/CONFIG_GET.spec.ts | 36 +- packages/graph/lib/commands/CONFIG_GET.ts | 23 +- .../graph/lib/commands/CONFIG_SET.spec.ts | 30 +- packages/graph/lib/commands/CONFIG_SET.ts | 21 +- packages/graph/lib/commands/DELETE.spec.ts | 32 +- packages/graph/lib/commands/DELETE.ts | 13 +- packages/graph/lib/commands/EXPLAIN.spec.ts | 36 +- packages/graph/lib/commands/EXPLAIN.ts | 15 +- packages/graph/lib/commands/LIST.spec.ts | 30 +- packages/graph/lib/commands/LIST.ts | 13 +- packages/graph/lib/commands/PROFILE.spec.ts | 30 +- packages/graph/lib/commands/PROFILE.ts | 15 +- packages/graph/lib/commands/QUERY.spec.ts | 68 +- packages/graph/lib/commands/QUERY.ts | 137 +- packages/graph/lib/commands/RO_QUERY.spec.ts | 32 +- packages/graph/lib/commands/RO_QUERY.ts | 32 +- packages/graph/lib/commands/SLOWLOG.spec.ts | 30 +- packages/graph/lib/commands/SLOWLOG.ts | 45 +- packages/graph/lib/commands/index.spec.ts | 62 - packages/graph/lib/commands/index.ts | 141 +- packages/graph/lib/graph.spec.ts | 286 +- packages/graph/lib/graph.ts | 568 +- packages/graph/lib/test-utils.ts | 21 +- packages/graph/package.json | 28 +- packages/json/.npmignore | 6 - packages/json/.release-it.json | 1 + packages/json/README.md | 24 +- packages/json/lib/commands/ARRAPPEND.spec.ts | 46 +- packages/json/lib/commands/ARRAPPEND.ts | 30 +- packages/json/lib/commands/ARRINDEX.spec.ts | 69 +- packages/json/lib/commands/ARRINDEX.ts | 36 +- packages/json/lib/commands/ARRINSERT.spec.ts | 46 +- packages/json/lib/commands/ARRINSERT.ts | 32 +- packages/json/lib/commands/ARRLEN.spec.ts | 48 +- packages/json/lib/commands/ARRLEN.ts | 21 +- packages/json/lib/commands/ARRPOP.spec.ts | 113 +- packages/json/lib/commands/ARRPOP.ts | 43 +- packages/json/lib/commands/ARRTRIM.spec.ts | 32 +- packages/json/lib/commands/ARRTRIM.ts | 13 +- packages/json/lib/commands/CLEAR.spec.ts | 32 + packages/json/lib/commands/CLEAR.ts | 20 + .../json/lib/commands/DEBUG_MEMORY.spec.ts | 46 +- packages/json/lib/commands/DEBUG_MEMORY.ts | 21 +- packages/json/lib/commands/DEL.spec.ts | 47 +- packages/json/lib/commands/DEL.ts | 21 +- packages/json/lib/commands/FORGET.spec.ts | 46 +- packages/json/lib/commands/FORGET.ts | 21 +- packages/json/lib/commands/GET.spec.ts | 105 +- packages/json/lib/commands/GET.ts | 50 +- packages/json/lib/commands/MERGE.spec.ts | 32 +- packages/json/lib/commands/MERGE.ts | 21 +- packages/json/lib/commands/MGET.spec.ts | 30 +- packages/json/lib/commands/MGET.ts | 28 +- packages/json/lib/commands/MSET.spec.ts | 62 +- packages/json/lib/commands/MSET.ts | 34 +- packages/json/lib/commands/NUMINCRBY.spec.ts | 32 +- packages/json/lib/commands/NUMINCRBY.ts | 18 +- packages/json/lib/commands/NUMMULTBY.spec.ts | 32 +- packages/json/lib/commands/NUMMULTBY.ts | 14 +- packages/json/lib/commands/OBJKEYS.spec.ts | 46 +- packages/json/lib/commands/OBJKEYS.ts | 21 +- packages/json/lib/commands/OBJLEN.spec.ts | 46 +- packages/json/lib/commands/OBJLEN.ts | 21 +- packages/json/lib/commands/RESP.spec.ts | 2 +- packages/json/lib/commands/SET.spec.ts | 56 +- packages/json/lib/commands/SET.ts | 45 +- packages/json/lib/commands/STRAPPEND.spec.ts | 48 +- packages/json/lib/commands/STRAPPEND.ts | 27 +- packages/json/lib/commands/STRLEN.spec.ts | 48 +- packages/json/lib/commands/STRLEN.ts | 21 +- packages/json/lib/commands/TOGGLE.spec.ts | 21 + packages/json/lib/commands/TOGGLE.ts | 10 + packages/json/lib/commands/TYPE.spec.ts | 46 +- packages/json/lib/commands/TYPE.ts | 26 +- packages/json/lib/commands/index.ts | 174 +- packages/json/lib/test-utils.ts | 22 +- packages/json/package.json | 28 +- .../redis/.release-it.json | 0 packages/redis/README.md | 203 + packages/redis/index.ts | 87 + packages/redis/package.json | 34 + packages/redis/tsconfig.json | 9 + packages/search/.npmignore | 6 - packages/search/.release-it.json | 1 + packages/search/README.md | 10 +- .../search/lib/commands/AGGREGATE.spec.ts | 955 ++-- packages/search/lib/commands/AGGREGATE.ts | 513 +- .../lib/commands/AGGREGATE_WITHCURSOR.spec.ts | 68 +- .../lib/commands/AGGREGATE_WITHCURSOR.ts | 73 +- packages/search/lib/commands/ALIASADD.spec.ts | 31 +- packages/search/lib/commands/ALIASADD.ts | 13 +- packages/search/lib/commands/ALIASDEL.spec.ts | 32 +- packages/search/lib/commands/ALIASDEL.ts | 13 +- .../search/lib/commands/ALIASUPDATE.spec.ts | 31 +- packages/search/lib/commands/ALIASUPDATE.ts | 13 +- packages/search/lib/commands/ALTER.spec.ts | 60 +- packages/search/lib/commands/ALTER.ts | 15 +- .../search/lib/commands/CONFIG_GET.spec.ts | 42 +- packages/search/lib/commands/CONFIG_GET.ts | 26 +- .../search/lib/commands/CONFIG_SET.spec.ts | 25 +- packages/search/lib/commands/CONFIG_SET.ts | 17 +- packages/search/lib/commands/CREATE.spec.ts | 877 ++-- packages/search/lib/commands/CREATE.ts | 349 +- .../search/lib/commands/CURSOR_DEL.spec.ts | 55 +- packages/search/lib/commands/CURSOR_DEL.ts | 22 +- .../search/lib/commands/CURSOR_READ.spec.ts | 75 +- packages/search/lib/commands/CURSOR_READ.ts | 38 +- packages/search/lib/commands/DICTADD.spec.ts | 44 +- packages/search/lib/commands/DICTADD.ts | 17 +- packages/search/lib/commands/DICTDEL.spec.ts | 44 +- packages/search/lib/commands/DICTDEL.ts | 17 +- packages/search/lib/commands/DICTDUMP.spec.ts | 32 +- packages/search/lib/commands/DICTDUMP.ts | 16 +- .../search/lib/commands/DROPINDEX.spec.ts | 52 +- packages/search/lib/commands/DROPINDEX.ts | 22 +- packages/search/lib/commands/EXPLAIN.spec.ts | 67 +- packages/search/lib/commands/EXPLAIN.ts | 34 +- .../search/lib/commands/EXPLAINCLI.spec.ts | 16 +- packages/search/lib/commands/EXPLAINCLI.ts | 13 +- packages/search/lib/commands/INFO.spec.ts | 91 +- packages/search/lib/commands/INFO.ts | 316 +- .../lib/commands/PROFILE_AGGREGATE.spec.ts | 17 +- .../search/lib/commands/PROFILE_AGGREGATE.ts | 57 +- .../lib/commands/PROFILE_SEARCH.spec.ts | 14 +- .../search/lib/commands/PROFILE_SEARCH.ts | 157 +- packages/search/lib/commands/SEARCH.spec.ts | 561 +- packages/search/lib/commands/SEARCH.ts | 301 +- .../lib/commands/SEARCH_NOCONTENT.spec.ts | 65 +- .../search/lib/commands/SEARCH_NOCONTENT.ts | 51 +- .../search/lib/commands/SPELLCHECK.spec.ts | 139 +- packages/search/lib/commands/SPELLCHECK.ts | 83 +- packages/search/lib/commands/SUGADD.spec.ts | 56 +- packages/search/lib/commands/SUGADD.ts | 23 +- packages/search/lib/commands/SUGDEL.spec.ts | 30 +- packages/search/lib/commands/SUGDEL.ts | 13 +- packages/search/lib/commands/SUGGET.spec.ts | 74 +- packages/search/lib/commands/SUGGET.ts | 25 +- .../lib/commands/SUGGET_WITHPAYLOADS.spec.ts | 56 +- .../lib/commands/SUGGET_WITHPAYLOADS.ts | 50 +- .../lib/commands/SUGGET_WITHSCORES.spec.ts | 54 +- .../search/lib/commands/SUGGET_WITHSCORES.ts | 67 +- .../SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts | 58 +- .../SUGGET_WITHSCORES_WITHPAYLOADS.ts | 74 +- packages/search/lib/commands/SUGLEN.spec.ts | 30 +- packages/search/lib/commands/SUGLEN.ts | 13 +- packages/search/lib/commands/SYNDUMP.spec.ts | 38 +- packages/search/lib/commands/SYNDUMP.ts | 25 +- .../search/lib/commands/SYNUPDATE.spec.ts | 66 +- packages/search/lib/commands/SYNUPDATE.ts | 33 +- packages/search/lib/commands/TAGVALS.spec.ts | 38 +- packages/search/lib/commands/TAGVALS.ts | 16 +- packages/search/lib/commands/_LIST.spec.ts | 28 +- packages/search/lib/commands/_LIST.ts | 16 +- packages/search/lib/commands/index.spec.ts | 6 +- packages/search/lib/commands/index.ts | 789 +-- packages/search/lib/index.ts | 8 +- packages/search/lib/test-utils.ts | 6 +- packages/search/package.json | 28 +- packages/test-utils/lib/dockers.ts | 374 +- packages/test-utils/lib/index.ts | 471 +- packages/test-utils/package.json | 19 +- packages/test-utils/tsconfig.json | 5 +- packages/time-series/.npmignore | 6 - packages/time-series/.release-it.json | 1 + packages/time-series/README.md | 180 +- packages/time-series/lib/commands/ADD.spec.ts | 159 +- packages/time-series/lib/commands/ADD.ts | 67 +- .../time-series/lib/commands/ALTER.spec.ts | 143 +- packages/time-series/lib/commands/ALTER.ts | 26 +- .../time-series/lib/commands/CREATE.spec.ts | 161 +- packages/time-series/lib/commands/CREATE.ts | 46 +- .../lib/commands/CREATERULE.spec.ts | 51 +- .../time-series/lib/commands/CREATERULE.ts | 53 +- .../time-series/lib/commands/DECRBY.spec.ts | 151 +- packages/time-series/lib/commands/DECRBY.ts | 17 +- packages/time-series/lib/commands/DEL.spec.ts | 32 +- packages/time-series/lib/commands/DEL.ts | 23 +- .../lib/commands/DELETERULE.spec.ts | 40 +- .../time-series/lib/commands/DELETERULE.ts | 19 +- packages/time-series/lib/commands/GET.spec.ts | 74 +- packages/time-series/lib/commands/GET.ts | 45 +- .../time-series/lib/commands/INCRBY.spec.ts | 169 +- packages/time-series/lib/commands/INCRBY.ts | 52 +- .../time-series/lib/commands/INFO.spec.ts | 17 +- packages/time-series/lib/commands/INFO.ts | 194 +- .../lib/commands/INFO_DEBUG.spec.ts | 19 +- .../time-series/lib/commands/INFO_DEBUG.ts | 122 +- .../time-series/lib/commands/MADD.spec.ts | 70 +- packages/time-series/lib/commands/MADD.ts | 32 +- .../time-series/lib/commands/MGET.spec.ts | 71 +- packages/time-series/lib/commands/MGET.ts | 76 +- .../lib/commands/MGET_SELECTED_LABELS.spec.ts | 46 + .../lib/commands/MGET_SELECTED_LABELS.ts | 16 + .../lib/commands/MGET_WITHLABELS.spec.ts | 72 +- .../lib/commands/MGET_WITHLABELS.ts | 86 +- .../time-series/lib/commands/MRANGE.spec.ts | 102 +- packages/time-series/lib/commands/MRANGE.ts | 65 +- .../lib/commands/MRANGE_GROUPBY.spec.ts | 66 + .../lib/commands/MRANGE_GROUPBY.ts | 108 + .../commands/MRANGE_SELECTED_LABELS.spec.ts | 72 + .../lib/commands/MRANGE_SELECTED_LABELS.ts | 70 + .../MRANGE_SELECTED_LABELS_GROUPBY.spec.ts | 80 + .../MRANGE_SELECTED_LABELS_GROUPBY.ts | 63 + .../lib/commands/MRANGE_WITHLABELS.spec.ts | 106 +- .../lib/commands/MRANGE_WITHLABELS.ts | 85 +- .../MRANGE_WITHLABELS_GROUPBY.spec.ts | 77 + .../lib/commands/MRANGE_WITHLABELS_GROUPBY.ts | 79 + .../lib/commands/MREVRANGE.spec.ts | 102 +- .../time-series/lib/commands/MREVRANGE.ts | 28 +- .../lib/commands/MREVRANGE_GROUPBY.spec.ts | 67 + .../lib/commands/MREVRANGE_GROUPBY.ts | 9 + .../MREVRANGE_SELECTED_LABELS.spec.ts | 73 + .../lib/commands/MREVRANGE_SELECTED_LABELS.ts | 9 + .../MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts | 80 + .../MREVRANGE_SELECTED_LABELS_GROUPBY.ts | 9 + .../lib/commands/MREVRANGE_WITHLABELS.spec.ts | 106 +- .../lib/commands/MREVRANGE_WITHLABELS.ts | 28 +- .../MREVRANGE_WITHLABELS_GROUPBY.spec.ts | 77 + .../commands/MREVRANGE_WITHLABELS_GROUPBY.ts | 9 + .../lib/commands/QUERYINDEX.spec.ts | 56 +- .../time-series/lib/commands/QUERYINDEX.ts | 23 +- .../time-series/lib/commands/RANGE.spec.ts | 68 +- packages/time-series/lib/commands/RANGE.ts | 135 +- .../time-series/lib/commands/REVRANGE.spec.ts | 142 +- packages/time-series/lib/commands/REVRANGE.ts | 33 +- .../time-series/lib/commands/index.spec.ts | 862 ++- packages/time-series/lib/commands/index.ts | 764 ++- packages/time-series/lib/index.ts | 12 +- packages/time-series/lib/test-utils.ts | 21 +- packages/time-series/package.json | 28 +- tsconfig.base.json | 23 +- tsconfig.json | 25 +- 1174 files changed, 45668 insertions(+), 36011 deletions(-) create mode 100644 .github/release-drafter-base.yml delete mode 100644 .npmignore create mode 100644 benchmark/lib/ping/ioredis-auto-pipeline.js create mode 100644 benchmark/lib/ping/local-resp2.js create mode 100644 benchmark/lib/ping/local-resp3-buffer-proxy.js create mode 100644 benchmark/lib/ping/local-resp3-buffer.js create mode 100644 benchmark/lib/ping/local-resp3-module-with-flags.js create mode 100644 benchmark/lib/ping/local-resp3-module.js create mode 100644 benchmark/lib/ping/local-resp3.js create mode 100644 docs/RESP.md create mode 100644 docs/command-options.md delete mode 100644 docs/isolated-execution.md create mode 100644 docs/pool.md create mode 100644 docs/programmability.md create mode 100644 docs/scan-iterators.md create mode 100644 docs/sentinel.md create mode 100644 docs/todo.md create mode 100644 docs/transactions.md create mode 100644 docs/v4-to-v5.md create mode 100644 docs/v5.md delete mode 100644 index.ts delete mode 100644 packages/bloom/.npmignore delete mode 100644 packages/bloom/lib/commands/cuckoo/index.spec.ts delete mode 100644 packages/bloom/lib/commands/t-digest/index.spec.ts delete mode 100644 packages/client/.eslintrc.json delete mode 100644 packages/client/.npmignore create mode 100644 packages/client/lib/RESP/decoder.spec.ts create mode 100644 packages/client/lib/RESP/decoder.ts create mode 100644 packages/client/lib/RESP/encoder.spec.ts create mode 100644 packages/client/lib/RESP/encoder.ts create mode 100644 packages/client/lib/RESP/types.ts create mode 100644 packages/client/lib/RESP/verbatim-string.ts delete mode 100644 packages/client/lib/client/RESP2/composers/buffer.spec.ts delete mode 100644 packages/client/lib/client/RESP2/composers/buffer.ts delete mode 100644 packages/client/lib/client/RESP2/composers/interface.ts delete mode 100644 packages/client/lib/client/RESP2/composers/string.spec.ts delete mode 100644 packages/client/lib/client/RESP2/composers/string.ts delete mode 100644 packages/client/lib/client/RESP2/decoder.spec.ts delete mode 100644 packages/client/lib/client/RESP2/decoder.ts delete mode 100644 packages/client/lib/client/RESP2/encoder.spec.ts delete mode 100644 packages/client/lib/client/RESP2/encoder.ts delete mode 100644 packages/client/lib/client/commands.ts create mode 100644 packages/client/lib/client/legacy-mode.spec.ts create mode 100644 packages/client/lib/client/legacy-mode.ts create mode 100644 packages/client/lib/client/linked-list.spec.ts create mode 100644 packages/client/lib/client/linked-list.ts create mode 100644 packages/client/lib/client/pool.spec.ts create mode 100644 packages/client/lib/client/pool.ts delete mode 100644 packages/client/lib/cluster/commands.ts delete mode 100644 packages/client/lib/command-options.ts delete mode 100644 packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.spec.ts delete mode 100644 packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.ts create mode 100644 packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts create mode 100644 packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts delete mode 100644 packages/client/lib/commands/GEORADIUSSTORE.spec.ts delete mode 100644 packages/client/lib/commands/GEORADIUSSTORE.ts create mode 100644 packages/client/lib/commands/GEORADIUS_STORE.spec.ts create mode 100644 packages/client/lib/commands/GEORADIUS_STORE.ts create mode 100644 packages/client/lib/commands/SPOP_COUNT.spec.ts create mode 100644 packages/client/lib/commands/SPOP_COUNT.ts delete mode 100644 packages/client/lib/commands/UNWATCH.spec.ts delete mode 100644 packages/client/lib/commands/UNWATCH.ts delete mode 100644 packages/client/lib/commands/WATCH.spec.ts delete mode 100644 packages/client/lib/commands/WATCH.ts create mode 100644 packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts create mode 100644 packages/client/lib/commands/XADD_NOMKSTREAM.ts create mode 100644 packages/client/lib/commands/ZADD_INCR.spec.ts create mode 100644 packages/client/lib/commands/ZADD_INCR.ts create mode 100644 packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts create mode 100644 packages/client/lib/commands/ZRANK_WITHSCORE.ts create mode 100644 packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts create mode 100644 packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts create mode 100644 packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts create mode 100644 packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts create mode 100644 packages/client/lib/sentinel/commands/SENTINEL_SET.ts create mode 100644 packages/client/lib/sentinel/commands/index.ts create mode 100644 packages/client/lib/sentinel/index.spec.ts create mode 100644 packages/client/lib/sentinel/index.ts create mode 100644 packages/client/lib/sentinel/module.ts create mode 100644 packages/client/lib/sentinel/multi-commands.ts create mode 100644 packages/client/lib/sentinel/pub-sub-proxy.ts create mode 100644 packages/client/lib/sentinel/test-util.ts create mode 100644 packages/client/lib/sentinel/types.ts create mode 100644 packages/client/lib/sentinel/utils.ts create mode 100644 packages/client/lib/sentinel/wait-queue.ts delete mode 100644 packages/client/lib/utils.ts delete mode 100644 packages/graph/lib/commands/index.spec.ts delete mode 100644 packages/json/.npmignore create mode 100644 packages/json/lib/commands/CLEAR.spec.ts create mode 100644 packages/json/lib/commands/CLEAR.ts create mode 100644 packages/json/lib/commands/TOGGLE.spec.ts create mode 100644 packages/json/lib/commands/TOGGLE.ts rename .release-it.json => packages/redis/.release-it.json (100%) create mode 100644 packages/redis/README.md create mode 100644 packages/redis/index.ts create mode 100644 packages/redis/package.json create mode 100644 packages/redis/tsconfig.json delete mode 100644 packages/search/.npmignore delete mode 100644 packages/time-series/.npmignore create mode 100644 packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts create mode 100644 packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts create mode 100644 packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts create mode 100644 packages/time-series/lib/commands/MRANGE_GROUPBY.ts create mode 100644 packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts create mode 100644 packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts create mode 100644 packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts create mode 100644 packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts create mode 100644 packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts create mode 100644 packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts create mode 100644 packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts create mode 100644 packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts create mode 100644 packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts create mode 100644 packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts create mode 100644 packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts create mode 100644 packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts create mode 100644 packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts create mode 100644 packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts diff --git a/.github/release-drafter-base.yml b/.github/release-drafter-base.yml new file mode 100644 index 00000000000..ea259fc0d2d --- /dev/null +++ b/.github/release-drafter-base.yml @@ -0,0 +1,50 @@ +name-template: 'json@$NEXT_PATCH_VERSION' +tag-template: 'json@$NEXT_PATCH_VERSION' +autolabeler: + - label: 'chore' + files: + - '*.md' + - '.github/*' + - label: 'bug' + branch: + - '/bug-.+' + - label: 'chore' + branch: + - '/chore-.+' + - label: 'feature' + branch: + - '/feature-.+' +categories: + - title: 'Breaking Changes' + labels: + - 'breakingchange' + - title: '🚀 New Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: + - 'chore' + - 'maintenance' + - 'documentation' + - 'docs' + +change-template: '- $TITLE (#$NUMBER)' +include-paths: + - 'packages/json' +exclude-labels: + - 'skip-changelog' +template: | + ## Changes + + $CHANGES + + ## Contributors + We'd like to thank all the contributors who worked on this release! + + $CONTRIBUTORS diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index e095faf1918..a8c22752423 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -17,8 +17,6 @@ jobs: uses: actions/setup-node@v3 - name: Install Packages run: npm ci - - name: Build tests tools - run: npm run build:tests-tools - name: Generate Documentation run: npm run documentation - name: Upload diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 84d70d6b4c0..68ea09c6e4f 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -5,11 +5,12 @@ on: branches: - master - v4.0 + - v5 pull_request: branches: - master - v4.0 - + - v5 jobs: tests: runs-on: ubuntu-latest @@ -17,7 +18,7 @@ jobs: fail-fast: false matrix: node-version: ['18', '20'] - redis-version: ['5', '6.0', '6.2', '7.0', '7.2', '7.4-rc2'] + redis-version: ['6.2.6-v17', '7.2.0-v13', '7.4.0-v1'] steps: - uses: actions/checkout@v4 with: @@ -31,10 +32,10 @@ jobs: if: ${{ matrix.node-version <= 14 }} - name: Install Packages run: npm ci - - name: Build tests tools - run: npm run build:tests-tools + - name: Build + run: npm run build - name: Run Tests - run: npm run test -- -- --forbid-only --redis-version=${{ matrix.redis-version }} + run: npm run test -ws --if-present -- --forbid-only --redis-version=${{ matrix.redis-version }} - name: Upload to Codecov run: | curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import diff --git a/.gitignore b/.gitignore index dfd47ff6716..ecdef37dffd 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ node_modules/ .DS_Store dump.rdb documentation/ +tsconfig.tsbuildinfo diff --git a/.npmignore b/.npmignore deleted file mode 100644 index a36c5e83cf2..00000000000 --- a/.npmignore +++ /dev/null @@ -1,12 +0,0 @@ -.github/ -.vscode/ -docs/ -examples/ -packages/ -.deepsource.toml -.release-it.json -CONTRIBUTING.md -SECURITY.md -index.ts -tsconfig.base.json -tsconfig.json diff --git a/README.md b/README.md index a590372b1b1..990204eb3df 100644 --- a/README.md +++ b/README.md @@ -25,25 +25,11 @@ node-redis is a modern, high performance [Redis](https://redis.io) client for No [Work at Redis](https://redis.com/company/careers/jobs/) -## Packages - -| Name | Description | -|----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| [redis](./) | [![Downloads](https://img.shields.io/npm/dm/redis.svg)](https://www.npmjs.com/package/redis) [![Version](https://img.shields.io/npm/v/redis.svg)](https://www.npmjs.com/package/redis) | -| [@redis/client](./packages/client) | [![Downloads](https://img.shields.io/npm/dm/@redis/client.svg)](https://www.npmjs.com/package/@redis/client) [![Version](https://img.shields.io/npm/v/@redis/client.svg)](https://www.npmjs.com/package/@redis/client) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/client/) | -| [@redis/bloom](./packages/bloom) | [![Downloads](https://img.shields.io/npm/dm/@redis/bloom.svg)](https://www.npmjs.com/package/@redis/bloom) [![Version](https://img.shields.io/npm/v/@redis/bloom.svg)](https://www.npmjs.com/package/@redis/bloom) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/bloom/) [Redis Bloom](https://oss.redis.com/redisbloom/) commands | -| [@redis/graph](./packages/graph) | [![Downloads](https://img.shields.io/npm/dm/@redis/graph.svg)](https://www.npmjs.com/package/@redis/graph) [![Version](https://img.shields.io/npm/v/@redis/graph.svg)](https://www.npmjs.com/package/@redis/graph) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/graph/) [Redis Graph](https://oss.redis.com/redisgraph/) commands | -| [@redis/json](./packages/json) | [![Downloads](https://img.shields.io/npm/dm/@redis/json.svg)](https://www.npmjs.com/package/@redis/json) [![Version](https://img.shields.io/npm/v/@redis/json.svg)](https://www.npmjs.com/package/@redis/json) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/json/) [Redis JSON](https://oss.redis.com/redisjson/) commands | -| [@redis/search](./packages/search) | [![Downloads](https://img.shields.io/npm/dm/@redis/search.svg)](https://www.npmjs.com/package/@redis/search) [![Version](https://img.shields.io/npm/v/@redis/search.svg)](https://www.npmjs.com/package/@redis/search) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/search/) [RediSearch](https://oss.redis.com/redisearch/) commands | -| [@redis/time-series](./packages/time-series) | [![Downloads](https://img.shields.io/npm/dm/@redis/time-series.svg)](https://www.npmjs.com/package/@redis/time-series) [![Version](https://img.shields.io/npm/v/@redis/time-series.svg)](https://www.npmjs.com/package/@redis/time-series) [![Docs](https://img.shields.io/badge/-documentation-dc382c)](https://redis.js.org/documentation/time-series/) [Redis Time-Series](https://oss.redis.com/redistimeseries/) commands | - -> :warning: In version 4.1.0 we moved our subpackages from `@node-redis` to `@redis`. If you're just using `npm install redis`, you don't need to do anything—it'll upgrade automatically. If you're using the subpackages directly, you'll need to point to the new scope (e.g. `@redis/client` instead of `@node-redis/client`). - ## Installation -Start a redis via docker: +Start a redis-server via docker (or any other method you prefer): -``` bash +```bash docker run -p 6379:6379 -it redis/redis-stack-server:latest ``` @@ -53,323 +39,21 @@ To install node-redis, simply: npm install redis ``` -> :warning: The new interface is clean and cool, but if you have an existing codebase, you'll want to read the [migration guide](./docs/v3-to-v4.md). - -Looking for a high-level library to handle object mapping? See [redis-om-node](https://github.com/redis/redis-om-node)! - -## Usage - -### Basic Example - -```typescript -import { createClient } from 'redis'; - -const client = await createClient() - .on('error', err => console.log('Redis Client Error', err)) - .connect(); - -await client.set('key', 'value'); -const value = await client.get('key'); -await client.disconnect(); -``` - -The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in the format `redis[s]://[[username][:password]@][host][:port][/db-number]`: - -```typescript -createClient({ - url: 'redis://alice:foobared@awesome.redis.server:6380' -}); -``` - -You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in the [client configuration guide](./docs/client-configuration.md). - -To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean. `client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it isn't (for example when the client is still connecting or reconnecting after a network error). - -### Redis Commands - -There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`, etc.): - -```typescript -// raw Redis commands -await client.HSET('key', 'field', 'value'); -await client.HGETALL('key'); - -// friendly JavaScript commands -await client.hSet('key', 'field', 'value'); -await client.hGetAll('key'); -``` - -Modifiers to commands are specified using a JavaScript object: - -```typescript -await client.set('key', 'value', { - EX: 10, - NX: true -}); -``` - -Replies will be transformed into useful data structures: - -```typescript -await client.hGetAll('key'); // { field1: 'value1', field2: 'value2' } -await client.hVals('key'); // ['value1', 'value2'] -``` - -`Buffer`s are supported as well: - -```typescript -await client.hSet('key', 'field', Buffer.from('value')); // 'OK' -await client.hGetAll( - commandOptions({ returnBuffers: true }), - 'key' -); // { field: } -``` - -### Unsupported Redis Commands - -If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`: - -```typescript -await client.sendCommand(['SET', 'key', 'value', 'NX']); // 'OK' - -await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2'] -``` - -### Transactions (Multi/Exec) - -Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When you're done, call `.exec()` and you'll get an array back with your results: - -```typescript -await client.set('another-key', 'another-value'); - -const [setKeyReply, otherKeyValue] = await client - .multi() - .set('key', 'value') - .get('another-key') - .exec(); // ['OK', 'another-value'] -``` - -You can also [watch](https://redis.io/topics/transactions#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change. - -To dig deeper into transactions, check out the [Isolated Execution Guide](./docs/isolated-execution.md). - -### Blocking Commands - -Any command can be run on a new connection by specifying the `isolated` option. The newly created connection is closed when the command's `Promise` is fulfilled. - -This pattern works especially well for blocking commands—such as `BLPOP` and `BLMOVE`: - -```typescript -import { commandOptions } from 'redis'; - -const blPopPromise = client.blPop( - commandOptions({ isolated: true }), - 'key', - 0 -); - -await client.lPush('key', ['1', '2']); - -await blPopPromise; // '2' -``` - -To learn more about isolated execution, check out the [guide](./docs/isolated-execution.md). - -### Pub/Sub - -See the [Pub/Sub overview](./docs/pub-sub.md). - -### Scan Iterator - -[`SCAN`](https://redis.io/commands/scan) results can be looped over using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator): - -```typescript -for await (const key of client.scanIterator()) { - // use the key! - await client.get(key); -} -``` - -This works with `HSCAN`, `SSCAN`, and `ZSCAN` too: - -```typescript -for await (const { field, value } of client.hScanIterator('hash')) {} -for await (const member of client.sScanIterator('set')) {} -for await (const { score, value } of client.zScanIterator('sorted-set')) {} -``` - -You can override the default options by providing a configuration object: - -```typescript -client.scanIterator({ - TYPE: 'string', // `SCAN` only - MATCH: 'patter*', - COUNT: 100 -}); -``` - -### [Programmability](https://redis.io/docs/manual/programmability/) - -Redis provides a programming interface allowing code execution on the redis server. - -#### [Functions](https://redis.io/docs/manual/programmability/functions-intro/) - -The following example retrieves a key in redis, returning the value of the key, incremented by an integer. For example, if your key _foo_ has the value _17_ and we run `add('foo', 25)`, it returns the answer to Life, the Universe and Everything. - -```lua -#!lua name=library - -redis.register_function { - function_name = 'add', - callback = function(keys, args) return redis.call('GET', keys[1]) + args[1] end, - flags = { 'no-writes' } -} -``` - -Here is the same example, but in a format that can be pasted into the `redis-cli`. - -``` -FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name=\"add\", callback=function(keys, args) return redis.call('GET', keys[1])+args[1] end, flags={\"no-writes\"}}" -``` - -Load the prior redis function on the _redis server_ before running the example below. - -```typescript -import { createClient } from 'redis'; - -const client = createClient({ - functions: { - library: { - add: { - NUMBER_OF_KEYS: 1, - transformArguments(key: string, toAdd: number): Array { - return [key, toAdd.toString()]; - }, - transformReply(reply: number): number { - return reply; - } - } - } - } -}); - -await client.connect(); - -await client.set('key', '1'); -await client.library.add('key', 2); // 3 -``` - -#### [Lua Scripts](https://redis.io/docs/manual/programmability/eval-intro/) - -The following is an end-to-end example of the prior concept. - -```typescript -import { createClient, defineScript } from 'redis'; - -const client = createClient({ - scripts: { - add: defineScript({ - NUMBER_OF_KEYS: 1, - SCRIPT: - 'return redis.call("GET", KEYS[1]) + ARGV[1];', - transformArguments(key: string, toAdd: number): Array { - return [key, toAdd.toString()]; - }, - transformReply(reply: number): number { - return reply; - } - }) - } -}); - -await client.connect(); - -await client.set('key', '1'); -await client.add('key', 2); // 3 -``` - -### Disconnecting - -There are two functions that disconnect a client from the Redis server. In most scenarios you should use `.quit()` to ensure that pending commands are sent to Redis before closing a connection. - -#### `.QUIT()`/`.quit()` - -Gracefully close a client's connection to Redis, by sending the [`QUIT`](https://redis.io/commands/quit) command to the server. Before quitting, the client executes any remaining commands in its queue, and will receive replies from Redis for each of them. - -```typescript -const [ping, get, quit] = await Promise.all([ - client.ping(), - client.get('key'), - client.quit() -]); // ['PONG', null, 'OK'] - -try { - await client.get('key'); -} catch (err) { - // ClosedClient Error -} -``` +> "redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands, you can install the individual packages. See the list below. -#### `.disconnect()` - -Forcibly close a client's connection to Redis immediately. Calling `disconnect` will not send further pending commands to the Redis server, or wait for or parse outstanding responses. - -```typescript -await client.disconnect(); -``` - -### Auto-Pipelining - -Node Redis will automatically pipeline requests that are made during the same "tick". - -```typescript -client.set('Tm9kZSBSZWRpcw==', 'users:1'); -client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw=='); -``` - -Of course, if you don't do something with your Promises you're certain to get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take advantage of auto-pipelining and handle your Promises, use `Promise.all()`. - -```typescript -await Promise.all([ - client.set('Tm9kZSBSZWRpcw==', 'users:1'), - client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==') -]); -``` - -### Clustering - -Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to connect to a Redis Cluster. - -### Events - -The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes: - -| Name | When | Listener arguments | -|-------------------------|------------------------------------------------------------------------------------|------------------------------------------------------------| -| `connect` | Initiating a connection to the server | *No arguments* | -| `ready` | Client is ready to use | *No arguments* | -| `end` | Connection has been closed (via `.quit()` or `.disconnect()`) | *No arguments* | -| `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` | -| `reconnecting` | Client is trying to reconnect to the server | *No arguments* | -| `sharded-channel-moved` | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | - -> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and an `error` occurs, that error will be thrown and the Node.js process will exit. See the [`EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details. - -> The client will not emit [any other events](./docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. - -## Supported Redis versions - -Node Redis is supported with the following versions of Redis: - -| Version | Supported | -|---------|--------------------| -| 7.0.z | :heavy_check_mark: | -| 6.2.z | :heavy_check_mark: | -| 6.0.z | :heavy_check_mark: | -| 5.0.z | :heavy_check_mark: | -| < 5.0 | :x: | +## Packages -> Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support. +| Name | Description | +|------------------------------------------------|---------------------------------------------------------------------------------------------| +| [`redis`](./packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | +| [`@redis/client`](./packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | +| [`@redis/bloom`](./packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | +| [`@redis/graph`](./packages/graph) | [Redis Graph](https://redis.io/docs/data-types/probabilistic/) commands | +| [`@redis/json`](./packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | +| [`@redis/search`](./packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | +| [`@redis/time-series`](./packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | + +> Looking for a high-level library to handle object mapping? See [redis-om-node](https://github.com/redis/redis-om-node)! ## Contributing diff --git a/benchmark/lib/index.js b/benchmark/lib/index.js index 15c8a12f401..5576999bfbc 100644 --- a/benchmark/lib/index.js +++ b/benchmark/lib/index.js @@ -1,10 +1,10 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { promises as fs } from 'fs'; -import { fork } from 'child_process'; -import { URL, fileURLToPath } from 'url'; -import { once } from 'events'; -import { extname } from 'path'; +import { promises as fs } from 'node:fs'; +import { fork } from 'node:child_process'; +import { URL, fileURLToPath } from 'node:url'; +import { once } from 'node:events'; +import { extname } from 'node:path'; async function getPathChoices() { const dirents = await fs.readdir(new URL('.', import.meta.url), { diff --git a/benchmark/lib/ping/ioredis-auto-pipeline.js b/benchmark/lib/ping/ioredis-auto-pipeline.js new file mode 100644 index 00000000000..ee400fe6ca9 --- /dev/null +++ b/benchmark/lib/ping/ioredis-auto-pipeline.js @@ -0,0 +1,20 @@ +import Redis from 'ioredis'; + +export default async (host) => { + const client = new Redis({ + host, + lazyConnect: true, + enableAutoPipelining: true + }); + + await client.connect(); + + return { + benchmark() { + return client.ping(); + }, + teardown() { + return client.disconnect(); + } + } +}; diff --git a/benchmark/lib/ping/local-resp2.js b/benchmark/lib/ping/local-resp2.js new file mode 100644 index 00000000000..873698a131f --- /dev/null +++ b/benchmark/lib/ping/local-resp2.js @@ -0,0 +1,21 @@ +import { createClient } from 'redis-local'; + +export default async (host) => { + const client = createClient({ + socket: { + host + }, + RESP: 2 + }); + + await client.connect(); + + return { + benchmark() { + return client.ping(); + }, + teardown() { + return client.disconnect(); + } + }; +}; diff --git a/benchmark/lib/ping/local-resp3-buffer-proxy.js b/benchmark/lib/ping/local-resp3-buffer-proxy.js new file mode 100644 index 00000000000..2ded38b21ca --- /dev/null +++ b/benchmark/lib/ping/local-resp3-buffer-proxy.js @@ -0,0 +1,23 @@ +import { createClient, RESP_TYPES } from 'redis-local'; + +export default async (host) => { + const client = createClient({ + socket: { + host + }, + RESP: 3 + }).withTypeMapping({ + [RESP_TYPES.SIMPLE_STRING]: Buffer + }); + + await client.connect(); + + return { + benchmark() { + return client.ping(); + }, + teardown() { + return client.disconnect(); + } + }; +}; diff --git a/benchmark/lib/ping/local-resp3-buffer.js b/benchmark/lib/ping/local-resp3-buffer.js new file mode 100644 index 00000000000..624a524ce06 --- /dev/null +++ b/benchmark/lib/ping/local-resp3-buffer.js @@ -0,0 +1,24 @@ +import { createClient, RESP_TYPES } from 'redis-local'; + +export default async (host) => { + const client = createClient({ + socket: { + host + }, + commandOptions: { + [RESP_TYPES.SIMPLE_STRING]: Buffer + }, + RESP: 3 + }); + + await client.connect(); + + return { + benchmark() { + return client.ping(); + }, + teardown() { + return client.disconnect(); + } + }; +}; diff --git a/benchmark/lib/ping/local-resp3-module-with-flags.js b/benchmark/lib/ping/local-resp3-module-with-flags.js new file mode 100644 index 00000000000..e58856dcb9e --- /dev/null +++ b/benchmark/lib/ping/local-resp3-module-with-flags.js @@ -0,0 +1,27 @@ +import { createClient } from 'redis-local'; +import PING from 'redis-local/dist/lib/commands/PING.js'; + +export default async (host) => { + const client = createClient({ + socket: { + host + }, + RESP: 3, + modules: { + module: { + ping: PING.default + } + } + }); + + await client.connect(); + + return { + benchmark() { + return client.withTypeMapping({}).module.ping(); + }, + teardown() { + return client.disconnect(); + } + }; +}; diff --git a/benchmark/lib/ping/local-resp3-module.js b/benchmark/lib/ping/local-resp3-module.js new file mode 100644 index 00000000000..66f6e3ec291 --- /dev/null +++ b/benchmark/lib/ping/local-resp3-module.js @@ -0,0 +1,27 @@ +import { createClient } from 'redis-local'; +import PING from 'redis-local/dist/lib/commands/PING.js'; + +export default async (host) => { + const client = createClient({ + socket: { + host + }, + RESP: 3, + modules: { + module: { + ping: PING.default + } + } + }); + + await client.connect(); + + return { + benchmark() { + return client.module.ping(); + }, + teardown() { + return client.disconnect(); + } + }; +}; diff --git a/benchmark/lib/ping/local-resp3.js b/benchmark/lib/ping/local-resp3.js new file mode 100644 index 00000000000..a4ee4f24a2a --- /dev/null +++ b/benchmark/lib/ping/local-resp3.js @@ -0,0 +1,21 @@ +import { createClient } from 'redis-local'; + +export default async (host) => { + const client = createClient({ + socket: { + host + }, + RESP: 3 + }); + + await client.connect(); + + return { + benchmark() { + return client.ping(); + }, + teardown() { + return client.disconnect(); + } + }; +}; diff --git a/benchmark/lib/ping/v3.js b/benchmark/lib/ping/v3.js index 26f269a42cf..e7e62d3e15a 100644 --- a/benchmark/lib/ping/v3.js +++ b/benchmark/lib/ping/v3.js @@ -1,6 +1,6 @@ import { createClient } from 'redis-v3'; -import { once } from 'events'; -import { promisify } from 'util'; +import { once } from 'node:events'; +import { promisify } from 'node:util'; export default async (host) => { const client = createClient({ host }), diff --git a/benchmark/lib/ping/v4.js b/benchmark/lib/ping/v4.js index 69aa3c06929..c570aa1477f 100644 --- a/benchmark/lib/ping/v4.js +++ b/benchmark/lib/ping/v4.js @@ -1,4 +1,4 @@ -import { createClient } from '@redis/client'; +import { createClient } from 'redis-v4'; export default async (host) => { const client = createClient({ diff --git a/benchmark/lib/runner.js b/benchmark/lib/runner.js index a96ff55cab0..7d81d3bb8c7 100644 --- a/benchmark/lib/runner.js +++ b/benchmark/lib/runner.js @@ -1,7 +1,7 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { basename } from 'path'; -import { promises as fs } from 'fs'; +import { basename } from 'node:path'; +import { promises as fs } from 'node:fs'; import * as hdr from 'hdr-histogram-js'; hdr.initWebAssemblySync(); @@ -71,7 +71,7 @@ const benchmarkStart = process.hrtime.bigint(), histogram = await run(times), benchmarkNanoseconds = process.hrtime.bigint() - benchmarkStart, json = { - timestamp, + // timestamp, operationsPerSecond: times / Number(benchmarkNanoseconds) * 1_000_000_000, p0: histogram.getValueAtPercentile(0), p50: histogram.getValueAtPercentile(50), diff --git a/benchmark/lib/set-get-delete-string/index.js b/benchmark/lib/set-get-delete-string/index.js index 719edfc7fdf..506b222a6cb 100644 --- a/benchmark/lib/set-get-delete-string/index.js +++ b/benchmark/lib/set-get-delete-string/index.js @@ -1,6 +1,6 @@ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; -import { randomBytes } from 'crypto'; +import { randomBytes } from 'node:crypto'; const { size } = yargs(hideBin(process.argv)) .option('size', { diff --git a/benchmark/lib/set-get-delete-string/v3.js b/benchmark/lib/set-get-delete-string/v3.js index 27ff6702a51..1e2122a0e49 100644 --- a/benchmark/lib/set-get-delete-string/v3.js +++ b/benchmark/lib/set-get-delete-string/v3.js @@ -1,6 +1,6 @@ import { createClient } from 'redis-v3'; -import { once } from 'events'; -import { promisify } from 'util'; +import { once } from 'node:events'; +import { promisify } from 'node:util'; export default async (host, { randomString }) => { const client = createClient({ host }), diff --git a/benchmark/package-lock.json b/benchmark/package-lock.json index 441c0b07b67..30114847134 100644 --- a/benchmark/package-lock.json +++ b/benchmark/package-lock.json @@ -6,11 +6,12 @@ "": { "name": "@redis/client-benchmark", "dependencies": { - "@redis/client": "../packages/client", "hdr-histogram-js": "3.0.0", - "ioredis": "5.3.2", - "redis-v3": "npm:redis@3.1.2", - "yargs": "17.7.2" + "ioredis": "5", + "redis-local": "file:../packages/client", + "redis-v3": "npm:redis@3", + "redis-v4": "npm:redis@4", + "yargs": "17.7.1" } }, "node_modules/@assemblyscript/loader": { @@ -23,10 +24,18 @@ "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==" }, + "node_modules/@redis/bloom": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", + "integrity": "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/@redis/client": { "version": "1.5.7", - "resolved": "file:../packages/client", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@redis/client/-/client-1.5.7.tgz", + "integrity": "sha512-gaOBOuJPjK5fGtxSseaKgSvjiZXQCdLlGg9WYQst+/GRUjmXaiB5kVkeQMRtPc7Q2t93XZcJfBMSwzs/XS9UZw==", "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", @@ -36,6 +45,38 @@ "node": ">=14" } }, + "node_modules/@redis/graph": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@redis/graph/-/graph-1.1.0.tgz", + "integrity": "sha512-16yZWngxyXPd+MJxeSr0dqh2AIOi8j9yXKcKCwVaKDbH3HTuETpDVPcLujhFYVPtYrngSco31BUcSa9TH31Gqg==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/json": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/json/-/json-1.0.4.tgz", + "integrity": "sha512-LUZE2Gdrhg0Rx7AN+cZkb1e6HjoSKaeeW8rYnt89Tly13GBI5eP4CwDVr+MY8BAYfCg4/N15OUrtLoona9uSgw==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/search": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@redis/search/-/search-1.1.2.tgz", + "integrity": "sha512-/cMfstG/fOh/SsE+4/BQGeuH/JJloeWuH+qJzM8dbxuWvdWibWAOAHHCZTMPhV3xIlH4/cUEIA8OV5QnYpaVoA==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, + "node_modules/@redis/time-series": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-1.0.4.tgz", + "integrity": "sha512-ThUIgo2U/g7cCuZavucQTQzA9g9JbDDY2f64u3AbAoz/8vE2lt2U37LamDUVChhaDA3IRT9R6VvJwqnUfTJzng==", + "peerDependencies": { + "@redis/client": "^1.0.0" + } + }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -244,6 +285,20 @@ "node": ">=4" } }, + "node_modules/redis-local": { + "name": "@redis/client", + "version": "1.5.6", + "resolved": "file:../packages/client", + "license": "MIT", + "dependencies": { + "cluster-key-slot": "1.1.2", + "generic-pool": "3.9.0", + "yallist": "4.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/redis-parser": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", @@ -282,6 +337,20 @@ "node": ">=0.10" } }, + "node_modules/redis-v4": { + "name": "redis", + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/redis/-/redis-4.6.6.tgz", + "integrity": "sha512-aLs2fuBFV/VJ28oLBqYykfnhGGkFxvx0HdCEBYdJ99FFbSEMZ7c1nVKwR6ZRv+7bb7JnC0mmCzaqu8frgOYhpA==", + "dependencies": { + "@redis/bloom": "1.2.0", + "@redis/client": "1.5.7", + "@redis/graph": "1.1.0", + "@redis/json": "1.0.4", + "@redis/search": "1.1.2", + "@redis/time-series": "1.0.4" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -349,9 +418,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.1.tgz", + "integrity": "sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", diff --git a/benchmark/package.json b/benchmark/package.json index f46f0e00b22..73acf9d0f1c 100644 --- a/benchmark/package.json +++ b/benchmark/package.json @@ -7,10 +7,11 @@ "start": "node ." }, "dependencies": { - "@redis/client": "../packages/client", "hdr-histogram-js": "3.0.0", - "ioredis": "5.3.2", - "redis-v3": "npm:redis@3.1.2", - "yargs": "17.7.2" + "ioredis": "5", + "redis-local": "file:../packages/client", + "redis-v3": "npm:redis@3", + "redis-v4": "npm:redis@4", + "yargs": "17.7.1" } } diff --git a/docs/RESP.md b/docs/RESP.md new file mode 100644 index 00000000000..5d634831e17 --- /dev/null +++ b/docs/RESP.md @@ -0,0 +1,46 @@ +# Mapping RESP types + +RESP, which stands for **R**edis **SE**rialization **P**rotocol, is the protocol used by Redis to communicate with clients. This document shows how RESP types can be mapped to JavaScript types. You can learn more about RESP itself in the [offical documentation](https://redis.io/docs/reference/protocol-spec/). + +By default, each type is mapped to the first option in the lists below. To change this, configure a [`typeMapping`](.). + +## RESP2 + +- Integer (`:`) => `number` +- Simple String (`+`) => `string | Buffer` +- Blob String (`$`) => `string | Buffer` +- Simple Error (`-`) => `ErrorReply` +- Array (`*`) => `Array` + +> NOTE: the first type is the default type + +## RESP3 + +- Null (`_`) => `null` +- Boolean (`#`) => `boolean` +- Number (`:`) => `number | string` +- Big Number (`(`) => `BigInt | string` +- Double (`,`) => `number | string` +- Simple String (`+`) => `string | Buffer` +- Blob String (`$`) => `string | Buffer` +- Verbatim String (`=`) => `string | Buffer | VerbatimString` (TODO: `VerbatimString` typedoc link) +- Simple Error (`-`) => `ErrorReply` +- Blob Error (`!`) => `ErrorReply` +- Array (`*`) => `Array` +- Set (`~`) => `Array | Set` +- Map (`%`) => `object | Map | Array` +- Push (`>`) => `Array` => PubSub push/`'push'` event + +> NOTE: the first type is the default type + +### Map keys and Set members + +When decoding a Map to `Map | object` or a Set to `Set`, keys and members of type "Simple String" or "Blob String" will be decoded as `string`s which enables lookups by value, ignoring type mapping. If you want them as `Buffer`s, decode them as `Array`s instead. + +### Not Implemented + +These parts of RESP3 are not yet implemented in Redis itself (at the time of writing), so are not yet implemented in the Node-Redis client either: + +- [Attribute type](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#attribute-type) +- [Streamed strings](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-strings) +- [Streamed aggregated data types](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md#streamed-aggregated-data-types) diff --git a/docs/client-configuration.md b/docs/client-configuration.md index 1854f07158a..deb68437e16 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -1,31 +1,32 @@ # `createClient` configuration -| Property | Default | Description | -|--------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| url | | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) | -| socket | | Socket connection properties. Unlisted [`net.connect`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) properties (and [`tls.connect`](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)) are also supported | -| socket.port | `6379` | Redis server port | -| socket.host | `'localhost'` | Redis server hostname | -| socket.family | `0` | IP Stack version (one of `4 \| 6 \| 0`) | -| socket.path | | Path to the UNIX Socket | -| socket.connectTimeout | `5000` | Connection Timeout (in milliseconds) | -| socket.noDelay | `true` | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) | -| socket.keepAlive | `5000` | Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) functionality | -| socket.tls | | See explanation and examples [below](#TLS) | -| socket.reconnectStrategy | `retries => Math.min(retries * 50, 500)` | A function containing the [Reconnect Strategy](#reconnect-strategy) logic | -| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) | -| password | | ACL password or the old "--requirepass" password | -| name | | Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) | -| database | | Redis database number (see [`SELECT`](https://redis.io/commands/select) command) | -| modules | | Included [Redis Modules](../README.md#packages) | -| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) | -| functions | | Function definitions (see [Functions](../README.md#functions)) | -| commandsQueueMaxLength | | Maximum length of the client's internal command queue | -| disableOfflineQueue | `false` | Disables offline queuing, see [FAQ](./FAQ.md#what-happens-when-the-network-goes-down) | -| 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) | -| pingInterval | | Send `PING` command at interval (in ms). Useful with ["Azure Cache for Redis"](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#idle-timeout) | +| Property | Default | Description | +|------------------------------|------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| url | | `redis[s]://[[username][:password]@][host][:port][/db-number]` (see [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details) | +| socket | | Socket connection properties. Unlisted [`net.connect`](https://nodejs.org/api/net.html#socketconnectoptions-connectlistener) properties (and [`tls.connect`](https://nodejs.org/api/tls.html#tlsconnectoptions-callback)) are also supported | +| socket.port | `6379` | Redis server port | +| socket.host | `'localhost'` | Redis server hostname | +| socket.family | `0` | IP Stack version (one of `4 \| 6 \| 0`) | +| socket.path | | Path to the UNIX Socket | +| socket.connectTimeout | `5000` | Connection timeout (in milliseconds) | +| socket.noDelay | `true` | Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) | +| socket.keepAlive | `true` | Toggle [`keep-alive`](https://nodejs.org/api/net.html#socketsetkeepaliveenable-initialdelay) functionality | +| socket.keepAliveInitialDelay | `5000` | If set to a positive number, it sets the initial delay before the first keepalive probe is sent on an idle socket | +| socket.tls | | See explanation and examples [below](#TLS) | +| socket.reconnectStrategy | Exponential backoff with a maximum of 2000 ms; plus 0-200 ms random jitter. | A function containing the [Reconnect Strategy](#reconnect-strategy) logic | +| username | | ACL username ([see ACL guide](https://redis.io/topics/acl)) | +| password | | ACL password or the old "--requirepass" password | +| name | | Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) | +| database | | Redis database number (see [`SELECT`](https://redis.io/commands/select) command) | +| modules | | Included [Redis Modules](../README.md#packages) | +| scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) | +| functions | | Function definitions (see [Functions](../README.md#functions)) | +| commandsQueueMaxLength | | Maximum length of the client's internal command queue | +| disableOfflineQueue | `false` | Disables offline queuing, see [FAQ](./FAQ.md#what-happens-when-the-network-goes-down) | +| 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) | +| pingInterval | | Send `PING` command at interval (in ms). Useful with ["Azure Cache for Redis"](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#idle-timeout) | ## Reconnect Strategy @@ -34,12 +35,19 @@ When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`), 2. `number` -> wait for `X` milliseconds before reconnecting. 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error. -By default the strategy is `Math.min(retries * 50, 500)`, but it can be overwritten like so: +By default the strategy uses exponential backoff, but it can be overwritten like so: ```javascript createClient({ socket: { - reconnectStrategy: retries => Math.min(retries * 50, 1000) + reconnectStrategy: retries => { + // Generate a random jitter between 0 – 200 ms: + const jitter = Math.floor(Math.random() * 200); + // Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms: + const delay = Math.min(Math.pow(2, retries) * 50, 2000); + + return delay + jitter; + } } }); ``` diff --git a/docs/clustering.md b/docs/clustering.md index 28ea0e2964c..f335c259c24 100644 --- a/docs/clustering.md +++ b/docs/clustering.md @@ -4,26 +4,22 @@ Connecting to a cluster is a bit different. Create the client by specifying some (or all) of the nodes in your cluster and then use it like a regular client instance: -```typescript +```javascript import { createCluster } from 'redis'; -const cluster = createCluster({ - rootNodes: [ - { +const cluster = await createCluster({ + rootNodes: [{ url: 'redis://10.0.0.1:30001' - }, - { + }, { url: 'redis://10.0.0.2:30002' - } - ] -}); - -cluster.on('error', (err) => console.log('Redis Cluster Error', err)); - -await cluster.connect(); + }] + }) + .on('error', err => console.log('Redis Cluster Error', err)) + .connect(); await cluster.set('key', 'value'); const value = await cluster.get('key'); +await cluster.close(); ``` ## `createCluster` configuration @@ -32,7 +28,7 @@ const value = await cluster.get('key'); | Property | Default | Description | |------------------------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| rootNodes | | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster, 3 should be enough to reliably connect and obtain the cluster configuration from the server | +| rootNodes | | An array of root nodes that are part of the cluster, which will be used to get the cluster topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the cluster configuration from the server | | defaults | | The default configuration values for every client in the cluster. Use this for example when specifying an ACL user to connect with | | useReplicas | `false` | When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes | | minimizeConnections | `false` | When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes. Useful for short-term or Pub/Sub-only connections. | @@ -41,9 +37,11 @@ const value = await cluster.get('key'); | modules | | Included [Redis Modules](../README.md#packages) | | scripts | | Script definitions (see [Lua Scripts](../README.md#lua-scripts)) | | functions | | Function definitions (see [Functions](../README.md#functions)) | + ## Auth with password and username Specifying the password in the URL or a root node will only affect the connection to that specific node. In case you want to set the password for all the connections being created from a cluster instance, use the `defaults` option. + ```javascript createCluster({ rootNodes: [{ @@ -107,7 +105,7 @@ createCluster({ ### Commands that operate on Redis Keys -Commands such as `GET`, `SET`, etc. are routed by the first key, for instance `MGET 1 2 3` will be routed by the key `1`. +Commands such as `GET`, `SET`, etc. are routed by the first key specified. For example `MGET 1 2 3` will be routed by the key `1`. ### [Server Commands](https://redis.io/commands#server) @@ -115,4 +113,4 @@ Admin commands such as `MEMORY STATS`, `FLUSHALL`, etc. are not attached to the ### "Forwarded Commands" -Certain commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. This client sends these commands to a random node in order to spread the load across the cluster. +Certain commands (e.g. `PUBLISH`) are forwarded to other cluster nodes by the Redis server. The client sends these commands to a random node in order to spread the load across the cluster. diff --git a/docs/command-options.md b/docs/command-options.md new file mode 100644 index 00000000000..b246445ad74 --- /dev/null +++ b/docs/command-options.md @@ -0,0 +1,68 @@ +# Command Options + +> :warning: The command options API in v5 has breaking changes from the previous version. For more details, refer to the [v4-to-v5 guide](./v4-to-v5.md#command-options). + +Command Options are used to create "proxy clients" that change the behavior of executed commands. See the sections below for details. + +## Type Mapping + +Some [RESP types](./RESP.md) can be mapped to more than one JavaScript type. For example, "Blob String" can be mapped to `string` or `Buffer`. You can override the default type mapping using the `withTypeMapping` function: + +```javascript +await client.get('key'); // `string | null` + +const proxyClient = client.withTypeMapping({ + [TYPES.BLOB_STRING]: Buffer +}); + +await proxyClient.get('key'); // `Buffer | null` +``` + +See [RESP](./RESP.md) for a full list of types. + +## Abort Signal + +The client [batches commands](./FAQ.md#how-are-commands-batched) before sending them to Redis. Commands that haven't been written to the socket yet can be aborted using the [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) API: + +```javascript +const controller = new AbortController(), + client = client.withAbortSignal(controller.signal); + +try { + const promise = client.get('key'); + controller.abort(); + await promise; +} catch (err) { + // AbortError +} +``` + +## ASAP + +Commands that are executed in the "asap" mode are added to the beginning of the "to sent" queue. + +```javascript +const asapClient = client.asap(); +await asapClient.ping(); +``` + +## `withCommandOptions` + +You can set all of the above command options in a single call with the `withCommandOptions` function: + +```javascript +client.withCommandOptions({ + typeMapping: ..., + abortSignal: ..., + asap: ... +}); +``` + +If any of the above options are omitted, the default value will be used. For example, the following client would **not** be in ASAP mode: + +```javascript +client.asap().withCommandOptions({ + typeMapping: ..., + abortSignal: ... +}); +``` diff --git a/docs/isolated-execution.md b/docs/isolated-execution.md deleted file mode 100644 index 7870a4680e7..00000000000 --- a/docs/isolated-execution.md +++ /dev/null @@ -1,67 +0,0 @@ -# Isolated Execution - -Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this: - -- You're using [transactions]() and need to `WATCH` a key or keys for changes. -- You want to run a blocking command that will take over the connection, such as `BLPOP` or `BLMOVE`. -- You're using the `MONITOR` command which also takes over a connection. - -Below are several examples of how to use isolated execution. - -> NOTE: Behind the scenes we're using [`generic-pool`](https://www.npmjs.com/package/generic-pool) to provide a pool of connections that can be isolated. Go there to learn more. - -## The Simple Scenario - -This just isolates execution on a single connection. Do what you want with that connection: - -```typescript -await client.executeIsolated(async isolatedClient => { - await isolatedClient.set('key', 'value'); - await isolatedClient.get('key'); -}); -``` - -## Transactions - -Things get a little more complex with transactions. Here we are `.watch()`ing some keys. If the keys change during the transaction, a `WatchError` is thrown when `.exec()` is called: - -```typescript -try { - await client.executeIsolated(async isolatedClient => { - await isolatedClient.watch('key'); - - const multi = isolatedClient.multi() - .ping() - .get('key'); - - if (Math.random() > 0.5) { - await isolatedClient.watch('another-key'); - multi.set('another-key', await isolatedClient.get('another-key') / 2); - } - - return multi.exec(); - }); -} catch (err) { - if (err instanceof WatchError) { - // the transaction aborted - } -} - -``` - -## Blocking Commands - -For blocking commands, you can execute a tidy little one-liner: - -```typescript -await client.executeIsolated(isolatedClient => isolatedClient.blPop('key')); -``` - -Or, you can just run the command directly, and provide the `isolated` option: - -```typescript -await client.blPop( - commandOptions({ isolated: true }), - 'key' -); -``` diff --git a/docs/pool.md b/docs/pool.md new file mode 100644 index 00000000000..7121e601d73 --- /dev/null +++ b/docs/pool.md @@ -0,0 +1,74 @@ +# `RedisClientPool` + +Sometimes you want to run your commands on an exclusive connection. There are a few reasons to do this: + +- You want to run a blocking command that will take over the connection, such as `BLPOP` or `BLMOVE`. +- You're using [transactions](https://redis.io/docs/interact/transactions/) and need to `WATCH` a key or keys for changes. +- Some more... + +For those use cases you'll need to create a connection pool. + +## Creating a pool + +You can create a pool using the `createClientPool` function: + +```javascript +import { createClientPool } from 'redis'; + +const pool = await createClientPool() + .on('error', err => console.error('Redis Client Pool Error', err)); +``` + +the function accepts two arguments, the client configuration (see [here](./client-configuration.md) for more details), and the pool configuration: + +| Property | Default | Description | +|----------------|---------|--------------------------------------------------------------------------------------------------------------------------------| +| minimum | 1 | The minimum clients the pool should hold to. The pool won't close clients if the pool size is less than the minimum. | +| maximum | 100 | The maximum clients the pool will have at once. The pool won't create any more resources and queue requests in memory. | +| acquireTimeout | 3000 | The maximum time (in ms) a task can wait in the queue. The pool will reject the task with `TimeoutError` in case of a timeout. | +| cleanupDelay | 3000 | The time to wait before cleaning up unused clients. | + +You can also create a pool from a client (reusing it's configuration): +```javascript +const pool = await client.createPool() + .on('error', err => console.error('Redis Client Pool Error', err)); +``` + +## The Simple Scenario + +All the client APIs are exposed on the pool instance directly, and will execute the commands using one of the available clients. + +```javascript +await pool.sendCommand(['PING']); // 'PONG' +await client.ping(); // 'PONG' +await client.withTypeMapping({ + [RESP_TYPES.SIMPLE_STRING]: Buffer +}).ping(); // Buffer +``` + +## Transactions + +Things get a little more complex with transactions. Here we are `.watch()`ing some keys. If the keys change during the transaction, a `WatchError` is thrown when `.exec()` is called: + +```javascript +try { + await pool.execute(async client => { + await client.watch('key'); + + const multi = client.multi() + .ping() + .get('key'); + + if (Math.random() > 0.5) { + await client.watch('another-key'); + multi.set('another-key', await client.get('another-key') / 2); + } + + return multi.exec(); + }); +} catch (err) { + if (err instanceof WatchError) { + // the transaction aborted + } +} +``` diff --git a/docs/programmability.md b/docs/programmability.md new file mode 100644 index 00000000000..f6ae42033c6 --- /dev/null +++ b/docs/programmability.md @@ -0,0 +1,76 @@ +# [Programmability](https://redis.io/docs/manual/programmability/) + +Redis provides a programming interface allowing code execution on the redis server. + +## [Functions](https://redis.io/docs/manual/programmability/functions-intro/) + +The following example retrieves a key in redis, returning the value of the key, incremented by an integer. For example, if your key _foo_ has the value _17_ and we run `add('foo', 25)`, it returns the answer to Life, the Universe and Everything. + +```lua +#!lua name=library + +redis.register_function { + function_name = 'add', + callback = function(keys, args) return redis.call('GET', keys[1]) + args[1] end, + flags = { 'no-writes' } +} +``` + +Here is the same example, but in a format that can be pasted into the `redis-cli`. + +``` +FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name='add', callback=function(keys, args) return redis.call('GET', keys[1])+args[1] end, flags={'no-writes'}}" +``` + +Load the prior redis function on the _redis server_ before running the example below. + +```typescript +import { createClient } from 'redis'; + +const client = createClient({ + functions: { + library: { + add: { + NUMBER_OF_KEYS: 1, + FIRST_KEY_INDEX: 1, + transformArguments(key: string, toAdd: number): Array { + return [key, toAdd.toString()]; + }, + transformReply: undefined as unknown as () => NumberReply + } + } + } +}); + +await client.connect(); + +await client.set('key', '1'); +await client.library.add('key', 2); // 3 +``` + +## [Lua Scripts](https://redis.io/docs/manual/programmability/eval-intro/) + +The following is an end-to-end example of the prior concept. + +```typescript +import { createClient, defineScript, NumberReply } from 'redis'; + +const client = createClient({ + scripts: { + add: defineScript({ + SCRIPT: 'return redis.call("GET", KEYS[1]) + ARGV[1];', + NUMBER_OF_KEYS: 1, + FIRST_KEY_INDEX: 1, + transformArguments(key: string, toAdd: number): Array { + return [key, toAdd.toString()]; + }, + transformReply: undefined as unknown as () => NumberReply + }) + } +}); + +await client.connect(); + +await client.set('key', '1'); +await client.add('key', 2); // 3 +``` diff --git a/docs/pub-sub.md b/docs/pub-sub.md index b319925569d..7bbb0733c18 100644 --- a/docs/pub-sub.md +++ b/docs/pub-sub.md @@ -1,18 +1,20 @@ # Pub/Sub -The Pub/Sub API is implemented by `RedisClient` and `RedisCluster`. +The Pub/Sub API is implemented by `RedisClient`, `RedisCluster`, and `RedisSentinel`. ## Pub/Sub with `RedisClient` -Pub/Sub requires a dedicated stand-alone client. You can easily get one by `.duplicate()`ing an existing `RedisClient`: +### RESP2 -```typescript +Using RESP2, Pub/Sub "takes over" the connection (a client with subscriptions will not execute commands), therefore it requires a dedicated connection. You can easily get one by `.duplicate()`ing an existing `RedisClient`: + +```javascript const subscriber = client.duplicate(); subscriber.on('error', err => console.error(err)); await subscriber.connect(); ``` -When working with a `RedisCluster`, this is handled automatically for you. +> When working with either `RedisCluster` or `RedisSentinel`, this is handled automatically for you. ### `sharded-channel-moved` event @@ -29,6 +31,8 @@ The event listener signature is as follows: ) ``` +> When working with `RedisCluster`, this is handled automatically for you. + ## Subscribing ```javascript @@ -39,7 +43,7 @@ await client.pSubscribe('channe*', listener); await client.sSubscribe('channel', listener); ``` -> ⚠️ Subscribing to the same channel more than once will create multiple listeners which will each be called when a message is recieved. +> ⚠️ Subscribing to the same channel more than once will create multiple listeners, each of which will be called when a message is received. ## Publishing diff --git a/docs/scan-iterators.md b/docs/scan-iterators.md new file mode 100644 index 00000000000..47c4d6c0567 --- /dev/null +++ b/docs/scan-iterators.md @@ -0,0 +1,30 @@ +# Scan Iterators + +> :warning: The scan iterators API in v5 has breaking changes from the previous version. For more details, refer to the [v4-to-v5 guide](./v4-to-v5.md#scan-iterators). + +[`SCAN`](https://redis.io/commands/scan) results can be looped over using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator): + +```javascript +for await (const keys of client.scanIterator()) { + const values = await client.mGet(keys); +} +``` + +This works with `HSCAN`, `SSCAN`, and `ZSCAN` too: + +```javascript +for await (const entries of client.hScanIterator('hash')) {} +for await (const members of client.sScanIterator('set')) {} +for await (const membersWithScores of client.zScanIterator('sorted-set')) {} +``` + +You can override the default options by providing a configuration object: + +```javascript +client.scanIterator({ + cursor: '0', // optional, defaults to '0' + TYPE: 'string', // `SCAN` only + MATCH: 'patter*', + COUNT: 100 +}); +``` diff --git a/docs/sentinel.md b/docs/sentinel.md new file mode 100644 index 00000000000..80e79c3f88c --- /dev/null +++ b/docs/sentinel.md @@ -0,0 +1,100 @@ +# Redis Sentinel + +The [Redis Sentinel](https://redis.io/docs/management/sentinel/) object of node-redis provides a high level object that provides access to a high availability redis installation managed by Redis Sentinel to provide enumeration of master and replica nodes belonging to an installation as well as reconfigure itself on demand for failover and topology changes. + +## Basic Example + +```javascript +import { createSentinel } from 'redis'; + +const sentinel = await createSentinel({ + name: 'sentinel-db', + sentinelRootNodes: [{ + host: 'example', + port: 1234 + }] + }) + .on('error', err => console.error('Redis Sentinel Error', err)); + .connect(); + +await sentinel.set('key', 'value'); +const value = await sentinel.get('key'); +await sentinel.close(); +``` + +In the above example, we configure the sentinel object to fetch the configuration for the database Redis Sentinel is monitoring as "sentinel-db" with one of the sentinels being located at `example:1234`, then using it like a regular Redis client. + +## `createSentinel` configuration + +| Property | Default | Description | +|-----------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | | The sentinel identifier for a particular database cluster | +| sentinelRootNodes | | An array of root nodes that are part of the sentinel cluster, which will be used to get the topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the sentinel configuration from the server | +| maxCommandRediscovers | `16` | The maximum number of times a command will retry due to topology changes. | +| nodeClientOptions | | The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with | +| sentinelClientOptions | | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with | +| masterPoolSize | `1` | The number of clients connected to the master node | +| replicaPoolSize | `0` | The number of clients connected to each replica node. When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes. | +| reserveClient | `false` | When `true`, one client will be reserved for the sentinel object. When `false`, the sentinel object will wait for the first available client from the pool. | +## PubSub + +It supports PubSub via the normal mechanisms, including migrating the listeners if the node they are connected to goes down. + +```javascript +await sentinel.subscribe('channel', message => { + // ... +}); +await sentinel.unsubscribe('channel'); +``` + +see [the PubSub guide](./pub-sub.md) for more details. + +## Sentinel as a pool + +The sentinel object provides the ability to manage a pool of clients for the master node: + +```javascript +createSentinel({ + // ... + masterPoolSize: 10 +}); +``` + +In addition, it also provides the ability have a pool of clients connected to the replica nodes, and to direct all read-only commands to them: + +```javascript +createSentinel({ + // ... + replicaPoolSize: 10 +}); +``` + +## Master client lease + +Sometimes multiple commands needs to run on an exclusive client (for example, using `WATCH/MULTI/EXEC`). + +There are 2 ways to get a client lease: + +`.use()` +```javascript +const result = await sentinel.use(async client => { + await client.watch('key'); + return client.multi() + .get('key') + .exec(); +}); +``` + +`.getMasterClientLease()` +```javascript +const clientLease = await sentinel.getMasterClientLease(); + +try { + await clientLease.watch('key'); + const resp = await clientLease.multi() + .get('key') + .exec(); +} finally { + clientLease.release(); +} +``` diff --git a/docs/todo.md b/docs/todo.md new file mode 100644 index 00000000000..49163444986 --- /dev/null +++ b/docs/todo.md @@ -0,0 +1,6 @@ +- "Isolation Pool" -> pool +- Cluster request response policies (either implement, or block "server" commands in cluster) + +Docs: +- [Command Options](./command-options.md) +- [RESP](./RESP.md) diff --git a/docs/transactions.md b/docs/transactions.md new file mode 100644 index 00000000000..6331fef4be5 --- /dev/null +++ b/docs/transactions.md @@ -0,0 +1,53 @@ +# [Transactions](https://redis.io/docs/interact/transactions/) ([`MULTI`](https://redis.io/commands/multi/)/[`EXEC`](https://redis.io/commands/exec/)) + +Start a [transaction](https://redis.io/docs/interact/transactions/) by calling `.multi()`, then chaining your commands. When you're done, call `.exec()` and you'll get an array back with your results: + +```javascript +const [setReply, getReply] = await client.multi() + .set('key', 'value') + .get('another-key') + .exec(); +``` + +## `exec<'typed'>()`/`execTyped()` + +A transaction invoked with `.exec<'typed'>`/`execTyped()` will return types appropriate to the commands in the transaction: + +```javascript +const multi = client.multi().ping(); +await multi.exec(); // Array +await multi.exec<'typed'>(); // [string] +await multi.execTyped(); // [string] +``` + +> :warning: this only works when all the commands are invoked in a single "call chain" + +## [`WATCH`](https://redis.io/commands/watch/) + +You can also [watch](https://redis.io/docs/interact/transactions/#optimistic-locking-using-check-and-set) keys by calling `.watch()`. Your transaction will abort if any of the watched keys change or if the client reconnected between the `watch` and `exec` calls. + +The `WATCH` state is stored on the connection (by the server). In case you need to run multiple `WATCH` & `MULTI` in parallel you'll need to use a [pool](./pool.md). + +## `execAsPipeline` + +`execAsPipeline` will execute the commands without "wrapping" it with `MULTI` & `EXEC` (and lose the transactional semantics). + +```javascript +await client.multi() + .get('a') + .get('b') + .execAsPipeline(); +``` + +the diffrence between the above pipeline and `Promise.all`: + +```javascript +await Promise.all([ + client.get('a'), + client.get('b') +]); +``` + +is that if the socket disconnects during the pipeline, any unwritten commands will be discarded. i.e. if the socket disconnects after `GET a` is written to the socket, but before `GET b` is: +- using `Promise.all` - the client will try to execute `GET b` when the socket reconnects +- using `execAsPipeline` - `GET b` promise will be rejected as well diff --git a/docs/v4-to-v5.md b/docs/v4-to-v5.md new file mode 100644 index 00000000000..95c2230ce23 --- /dev/null +++ b/docs/v4-to-v5.md @@ -0,0 +1,240 @@ +# v4 to v5 migration guide + +## Client Configuration + +### Keep Alive + +To better align with Node.js build-in [`net`](https://nodejs.org/api/net.html) and [`tls`](https://nodejs.org/api/tls.html) modules, the `keepAlive` option has been split into 2 options: `keepAlive` (`boolean`) and `keepAliveInitialDelay` (`number`). The defaults remain `true` and `5000`. + +### Legacy Mode + +In the previous version, you could access "legacy" mode by creating a client and passing in `{ legacyMode: true }`. Now, you can create one off of an existing client by calling the `.legacy()` function. This allows easier access to both APIs and enables better TypeScript support. + +```javascript +// use `client` for the current API +const client = createClient(); +await client.set('key', 'value'); + +// use `legacyClient` for the "legacy" API +const legacyClient = client.legacy(); +legacyClient.set('key', 'value', (err, reply) => { + // ... +}); +``` + +## Command Options + +In v4, command options are passed as a first optional argument: + +```javascript +await client.get('key'); // `string | null` +await client.get(client.commandOptions({ returnBuffers: true }), 'key'); // `Buffer | null` +``` + +This has a couple of flaws: +1. The argument types are checked in runtime, which is a performance hit. +2. Code suggestions are less readable/usable, due to "function overloading". +3. Overall, "user code" is not as readable as it could be. + +### The new API for v5 + +With the new API, instead of passing the options directly to the commands we use a "proxy client" to store them: + +```javascript +await client.get('key'); // `string | null` + +const proxyClient = client.withCommandOptions({ + typeMapping: { + [TYPES.BLOB_STRING]: Buffer + } +}); + +await proxyClient.get('key'); // `Buffer | null` +``` + +for more information, see the [Command Options guide](./command-options.md). + +## Quit VS Disconnect + +The `QUIT` command has been deprecated in Redis 7.2 and should now also be considered deprecated in Node-Redis. Instead of sending a `QUIT` command to the server, the client can simply close the network connection. + +`client.QUIT/quit()` is replaced by `client.close()`. and, to avoid confusion, `client.disconnect()` has been renamed to `client.destroy()`. + +## Scan Iterators + +Iterator commands like `SCAN`, `HSCAN`, `SSCAN`, and `ZSCAN` return collections of elements (depending on the data type). However, v4 iterators loop over these collections and yield individual items: + +```javascript +for await (const key of client.scanIterator()) { + console.log(key, await client.get(key)); +} +``` + +This mismatch can be awkward and makes "multi-key" commands like `MGET`, `UNLINK`, etc. pointless. So, in v5 the iterators now yield a collection instead of an element: + +```javascript +for await (const keys of client.scanIterator()) { + // we can now meaningfully utilize "multi-key" commands + console.log(keys, await client.mGet(keys)); +} +``` + +for more information, see the [Scan Iterators guide](./scan-iterators.md). + +## Isolation Pool + +In v4, `RedisClient` had the ability to create a pool of connections using an "Isolation Pool" on top of the "main" connection. However, there was no way to use the pool without a "main" connection: +```javascript +const client = await createClient() + .on('error', err => console.error(err)) + .connect(); + +await client.ping( + client.commandOptions({ isolated: true }) +); +``` + +In v5 we've extracted this pool logic into its own class—`RedisClientPool`: + +```javascript +const pool = await createClientPool() + .on('error', err => console.error(err)) + .connect(); + +await pool.ping(); +``` + +See the [pool guide](./pool.md) for more information. + +## Cluster `MULTI` + +In v4, `cluster.multi()` did not support executing commands on replicas, even if they were readonly. + +```javascript +// this might execute on a replica, depending on configuration +await cluster.sendCommand('key', true, ['GET', 'key']); + +// this always executes on a master +await cluster.multi() + .addCommand('key', ['GET', 'key']) + .exec(); +``` + +To support executing commands on replicas, `cluster.multi().addCommand` now requires `isReadonly` as the second argument, which matches the signature of `cluster.sendCommand`: + +```javascript +await cluster.multi() + .addCommand('key', true, ['GET', 'key']) + .exec(); +``` + +## `MULTI.execAsPipeline()` + +```javascript +await client.multi() + .set('a', 'a') + .set('b', 'b') + .execAsPipeline(); +``` + +In older versions, if the socket disconnects during the pipeline execution, i.e. after writing `SET a a` and before `SET b b`, the returned promise is rejected, but `SET b b` will still be executed on the server. + +In v5, any unwritten commands (in the same pipeline) will be discarded. + +## Commands + +### Redis + +- `ACL GETUSER`: `selectors` +- `COPY`: `destinationDb` -> `DB`, `replace` -> `REPLACE`, `boolean` -> `number` [^boolean-to-number] +- `CLIENT KILL`: `enum ClientKillFilters` -> `const CLIENT_KILL_FILTERS` [^enum-to-constants] +- `CLUSTER FAILOVER`: `enum FailoverModes` -> `const FAILOVER_MODES` [^enum-to-constants] +- `CLIENT TRACKINGINFO`: `flags` in RESP2 - `Set` -> `Array` (to match RESP3 default type mapping) +- `CLUSTER INFO`: +- `CLUSTER SETSLOT`: `ClusterSlotStates` -> `CLUSTER_SLOT_STATES` [^enum-to-constants] +- `CLUSTER RESET`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing] +- `CLUSTER FAILOVER`: `enum FailoverModes` -> `const FAILOVER_MODES` [^enum-to-constants], the second argument is `{ mode: string; }` instead of `string` [^future-proofing] +- `CLUSTER LINKS`: `createTime` -> `create-time`, `sendBufferAllocated` -> `send-buffer-allocated`, `sendBufferUsed` -> `send-buffer-used` [^map-keys] +- `CLUSTER NODES`, `CLUSTER REPLICAS`, `CLUSTER INFO`: returning the raw `VerbatimStringReply` +- `EXPIRE`: `boolean` -> `number` [^boolean-to-number] +- `EXPIREAT`: `boolean` -> `number` [^boolean-to-number] +- `HSCAN`: `tuples` has been renamed to `entries` +- `HEXISTS`: `boolean` -> `number` [^boolean-to-number] +- `HRANDFIELD_COUNT_WITHVALUES`: `Record` -> `Array<{ field: BlobString; value: BlobString; }>` (it can return duplicates). +- `HSETNX`: `boolean` -> `number` [^boolean-to-number] +- `INFO`: +- `LCS IDX`: `length` has been changed to `len`, `matches` has been changed from `Array<{ key1: RangeReply; key2: RangeReply; }>` to `Array<[key1: RangeReply, key2: RangeReply]>` + + +- `ZINTER`: instead of `client.ZINTER('key', { WEIGHTS: [1] })` use `client.ZINTER({ key: 'key', weight: 1 }])` +- `ZINTER_WITHSCORES`: instead of `client.ZINTER_WITHSCORES('key', { WEIGHTS: [1] })` use `client.ZINTER_WITHSCORES({ key: 'key', weight: 1 }])` +- `ZUNION`: instead of `client.ZUNION('key', { WEIGHTS: [1] })` use `client.ZUNION({ key: 'key', weight: 1 }])` +- `ZUNION_WITHSCORES`: instead of `client.ZUNION_WITHSCORES('key', { WEIGHTS: [1] })` use `client.ZUNION_WITHSCORES({ key: 'key', weight: 1 }])` +- `ZMPOP`: `{ elements: Array<{ member: string; score: number; }>; }` -> `{ members: Array<{ value: string; score: number; }>; }` to match other sorted set commands (e.g. `ZRANGE`, `ZSCAN`) + +- `MOVE`: `boolean` -> `number` [^boolean-to-number] +- `PEXPIRE`: `boolean` -> `number` [^boolean-to-number] +- `PEXPIREAT`: `boolean` -> `number` [^boolean-to-number] +- `PFADD`: `boolean` -> `number` [^boolean-to-number] + +- `RENAMENX`: `boolean` -> `number` [^boolean-to-number] +- `SETNX`: `boolean` -> `number` [^boolean-to-number] +- `SCAN`, `HSCAN`, `SSCAN`, and `ZSCAN`: `reply.cursor` will not be converted to number to avoid issues when the number is bigger than `Number.MAX_SAFE_INTEGER`. See [here](https://github.com/redis/node-redis/issues/2561). +- `SCRIPT EXISTS`: `Array` -> `Array` [^boolean-to-number] +- `SISMEMBER`: `boolean` -> `number` [^boolean-to-number] +- `SMISMEMBER`: `Array` -> `Array` [^boolean-to-number] +- `SMOVE`: `boolean` -> `number` [^boolean-to-number] + +- `GEOSEARCH_WITH`/`GEORADIUS_WITH`: `GeoReplyWith` -> `GEO_REPLY_WITH` [^enum-to-constants] +- `GEORADIUSSTORE` -> `GEORADIUS_STORE` +- `GEORADIUSBYMEMBERSTORE` -> `GEORADIUSBYMEMBER_STORE` +- `XACK`: `boolean` -> `number` [^boolean-to-number] +- `XADD`: the `INCR` option has been removed, use `XADD_INCR` instead +- `LASTSAVE`: `Date` -> `number` (unix timestamp) +- `HELLO`: `protover` moved from the options object to it's own argument, `auth` -> `AUTH`, `clientName` -> `SETNAME` +- `MODULE LIST`: `version` -> `ver` [^map-keys] +- `MEMORY STATS`: [^map-keys] +- `FUNCTION RESTORE`: the second argument is `{ mode: string; }` instead of `string` [^future-proofing] +- `FUNCTION STATS`: `runningScript` -> `running_script`, `durationMs` -> `duration_ms`, `librariesCount` -> `libraries_count`, `functionsCount` -> `functions_count` [^map-keys] + +- `TIME`: `Date` -> `[unixTimestamp: string, microseconds: string]` + +- `XGROUP_CREATECONSUMER`: [^boolean-to-number] +- `XGROUP_DESTROY`: [^boolean-to-number] +- `XINFO GROUPS`: `lastDeliveredId` -> `last-delivered-id` [^map-keys] +- `XINFO STREAM`: `radixTreeKeys` -> `radix-tree-keys`, `radixTreeNodes` -> `radix-tree-nodes`, `lastGeneratedId` -> `last-generated-id`, `maxDeletedEntryId` -> `max-deleted-entry-id`, `entriesAdded` -> `entries-added`, `recordedFirstEntryId` -> `recorded-first-entry-id`, `firstEntry` -> `first-entry`, `lastEntry` -> `last-entry` +- `XAUTOCLAIM`, `XCLAIM`, `XRANGE`, `XREVRANGE`: `Array<{ name: string; messages: Array<{ id: string; message: Record }>; }>` -> `Record }>>` + +- `COMMAND LIST`: `enum FilterBy` -> `const COMMAND_LIST_FILTER_BY` [^enum-to-constants], the filter argument has been moved from a "top level argument" into ` { FILTERBY: { type: ; value: } }` + +### Bloom + +- `TOPK.QUERY`: `Array` -> `Array` + +### Graph + +- `GRAPH.SLOWLOG`: `timestamp` has been changed from `Date` to `number` + +### JSON + +- `JSON.ARRINDEX`: `start` and `end` arguments moved to `{ range: { start: number; end: number; }; }` [^future-proofing] +- `JSON.ARRPOP`: `path` and `index` arguments moved to `{ path: string; index: number; }` [^future-proofing] +- `JSON.ARRLEN`, `JSON.CLEAR`, `JSON.DEBUG MEMORY`, `JSON.DEL`, `JSON.FORGET`, `JSON.OBJKEYS`, `JSON.OBJLEN`, `JSON.STRAPPEND`, `JSON.STRLEN`, `JSON.TYPE`: `path` argument moved to `{ path: string; }` [^future-proofing] + +### Search + +- `FT.SUGDEL`: [^boolean-to-number] +- `FT.CURSOR READ`: `cursor` type changed from `number` to `string` (in and out) to avoid issues when the number is bigger than `Number.MAX_SAFE_INTEGER`. See [here](https://github.com/redis/node-redis/issues/2561). + +### Time Series + +- `TS.ADD`: `boolean` -> `number` [^boolean-to-number] +- `TS.[M][REV]RANGE`: `enum TimeSeriesBucketTimestamp` -> `const TIME_SERIES_BUCKET_TIMESTAMP` [^enum-to-constants], `enum TimeSeriesReducers` -> `const TIME_SERIES_REDUCERS` [^enum-to-constants], the `ALIGN` argument has been moved into `AGGREGRATION` +- `TS.SYNUPDATE`: `Array>` -> `Record>` +- `TS.M[REV]RANGE[_WITHLABELS]`, `TS.MGET[_WITHLABELS]`: TODO + +[^enum-to-constants]: TODO + +[^map-keys]: To avoid unnecessary transformations and confusion, map keys will not be transformed to "js friendly" names (i.e. `number-of-keys` will not be renamed to `numberOfKeys`). See [here](https://github.com/redis/node-redis/discussions/2506). + +[^future-proofing]: TODO diff --git a/docs/v5.md b/docs/v5.md new file mode 100644 index 00000000000..4a1bd817b9b --- /dev/null +++ b/docs/v5.md @@ -0,0 +1,38 @@ +# RESP3 Support + +TODO + +```javascript +const client = createClient({ + RESP: 3 +}); +``` + +```javascript +// by default +await client.hGetAll('key'); // Record + +await client.withTypeMapping({ + [TYPES.MAP]: Map +}).hGetAll('key'); // Map + +await client.withTypeMapping({ + [TYPES.MAP]: Map, + [TYPES.BLOB_STRING]: Buffer +}).hGetAll('key'); // Map +``` + +# Sentinel Support + +[TODO](./sentinel.md) + +# `multi.exec<'typed'>` / `multi.execTyped` + +We have introduced the ability to perform a "typed" `MULTI`/`EXEC` transaction. Rather than returning `Array`, a transaction invoked with `.exec<'typed'>` will return types appropriate to the commands in the transaction where possible: + +```javascript +const multi = client.multi().ping(); +await multi.exec(); // Array +await multi.exec<'typed'>(); // [string] +await multi.execTyped(); // [string] +``` diff --git a/examples/README.md b/examples/README.md index 4e7655a3519..1080f424da1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -91,5 +91,5 @@ await client.connect(); // Add your example code here... -await client.quit(); +client.destroy(); ``` diff --git a/examples/blocking-list-pop.js b/examples/blocking-list-pop.js index 099c73a2a96..ec9bec4d639 100644 --- a/examples/blocking-list-pop.js +++ b/examples/blocking-list-pop.js @@ -27,4 +27,4 @@ console.log('blpopPromise resolved'); // {"key":"keyName","element":"value"} console.log(`listItem is '${JSON.stringify(listItem)}'`); -await client.quit(); +client.destroy(); diff --git a/examples/bloom-filter.js b/examples/bloom-filter.js index cf5f1940b3e..a133b0274f2 100644 --- a/examples/bloom-filter.js +++ b/examples/bloom-filter.js @@ -77,4 +77,4 @@ const info = await client.bf.info('mybloom'); // } console.log(info); -await client.quit(); +client.destroy(); diff --git a/examples/check-connection-status.js b/examples/check-connection-status.js index 0ccf8ff5e21..ae3c863fb14 100644 --- a/examples/check-connection-status.js +++ b/examples/check-connection-status.js @@ -25,4 +25,4 @@ console.log('Afer connectPromise has resolved...'); // isReady will return True here, client is ready to use. console.log(`client.isOpen: ${client.isOpen}, client.isReady: ${client.isReady}`); -await client.quit(); +client.destroy(); diff --git a/examples/command-with-modifiers.js b/examples/command-with-modifiers.js index 974f78dc5d8..31106b17e45 100644 --- a/examples/command-with-modifiers.js +++ b/examples/command-with-modifiers.js @@ -1,25 +1,31 @@ // Define a custom script that shows example of SET command // with several modifiers. -import { createClient } from 'redis'; +import { createClient } from '../packages/client'; const client = createClient(); await client.connect(); await client.del('mykey'); -let result = await client.set('mykey', 'myvalue', { - EX: 60, - GET: true -}); - -console.log(result); //null - -result = await client.set('mykey', 'newvalue', { - EX: 60, - GET: true -}); - -console.log(result); //myvalue - -await client.quit(); +console.log( + await client.set('mykey', 'myvalue', { + expiration: { + type: 'EX', + value: 60 + }, + GET: true + }) +); // null + +console.log( + await client.set('mykey', 'newvalue', { + expiration: { + type: 'EX', + value: 60 + }, + GET: true + }) +); // 'myvalue' + +await client.close(); diff --git a/examples/connect-as-acl-user.js b/examples/connect-as-acl-user.js index df46aa1e288..bc3069b5bbc 100644 --- a/examples/connect-as-acl-user.js +++ b/examples/connect-as-acl-user.js @@ -23,4 +23,4 @@ try { console.log(`GET command failed: ${e.message}`); } -await client.quit(); +client.destroy(); diff --git a/examples/count-min-sketch.js b/examples/count-min-sketch.js index f88a148986f..ffbe13a7c27 100644 --- a/examples/count-min-sketch.js +++ b/examples/count-min-sketch.js @@ -77,4 +77,4 @@ console.log('Count-Min Sketch info:'); // } console.log(info); -await client.quit(); +client.destroy(); diff --git a/examples/cuckoo-filter.js b/examples/cuckoo-filter.js index 87976f3fefb..6ab58fbfa5c 100644 --- a/examples/cuckoo-filter.js +++ b/examples/cuckoo-filter.js @@ -76,4 +76,4 @@ const info = await client.cf.info('mycuckoo'); // } console.log(info); -await client.quit(); +client.destroy(); diff --git a/examples/dump-and-restore.js b/examples/dump-and-restore.js index 081e44f9f9a..c2ee7f1e199 100644 --- a/examples/dump-and-restore.js +++ b/examples/dump-and-restore.js @@ -1,22 +1,26 @@ // This example demonstrates the use of the DUMP and RESTORE commands -import { commandOptions, createClient } from 'redis'; +import { createClient, RESP_TYPES } from 'redis'; -const client = createClient(); -await client.connect(); +const client = await createClient({ + commandOptions: { + typeMapping: { + [RESP_TYPES.BLOB_STRING]: Buffer + } + } +}).on('error', err => { + console.log('Redis Client Error', err); +}).connect(); // DUMP a specific key into a local variable -const dump = await client.dump( - commandOptions({ returnBuffers: true }), - 'source' -); +const dump = await client.dump('source'); // RESTORE into a new key await client.restore('destination', 0, dump); // RESTORE and REPLACE an existing key await client.restore('destination', 0, dump, { - REPLACE: true + REPLACE: true }); -await client.quit(); +await client.close(); diff --git a/examples/get-server-time.js b/examples/get-server-time.js index 967859f0136..0e32c1296af 100644 --- a/examples/get-server-time.js +++ b/examples/get-server-time.js @@ -9,4 +9,4 @@ const serverTime = await client.time(); // 2022-02-25T12:57:40.000Z { microseconds: 351346 } console.log(serverTime); -await client.quit(); +client.destroy(); diff --git a/examples/hyperloglog.js b/examples/hyperloglog.js index 4ac9b575f96..027112a08bf 100644 --- a/examples/hyperloglog.js +++ b/examples/hyperloglog.js @@ -48,4 +48,4 @@ try { console.error(e); } -await client.quit(); +client.destroy(); diff --git a/examples/lua-multi-incr.js b/examples/lua-multi-incr.js index 8eb1092c295..5cf39142006 100644 --- a/examples/lua-multi-incr.js +++ b/examples/lua-multi-incr.js @@ -24,4 +24,4 @@ await client.connect(); await client.set('mykey', '5'); console.log(await client.mincr('mykey', 'myotherkey', 10)); // [ 15, 10 ] -await client.quit(); +client.destroy(); diff --git a/examples/managing-json.js b/examples/managing-json.js index 81949d5c222..a28a0ee5106 100644 --- a/examples/managing-json.js +++ b/examples/managing-json.js @@ -73,4 +73,4 @@ const numPets = await client.json.arrLen('noderedis:jsondata', '$.pets'); // We now have 4 pets. console.log(`We now have ${numPets} pets.`); -await client.quit(); +client.destroy(); diff --git a/examples/package.json b/examples/package.json index 65ba1442f7e..91120774d94 100644 --- a/examples/package.json +++ b/examples/package.json @@ -6,7 +6,7 @@ "private": true, "type": "module", "dependencies": { - "redis": "../" + "redis": "../packages/client" } } diff --git a/examples/search-hashes.js b/examples/search-hashes.js index 2f8b5fbf7b6..f3aca6b8aed 100644 --- a/examples/search-hashes.js +++ b/examples/search-hashes.js @@ -85,4 +85,4 @@ for (const doc of results.documents) { console.log(`${doc.id}: ${doc.value.name}, ${doc.value.age} years old.`); } -await client.quit(); +client.destroy(); diff --git a/examples/search-json.js b/examples/search-json.js index 6481889ecfd..bff5b2cb362 100644 --- a/examples/search-json.js +++ b/examples/search-json.js @@ -145,4 +145,4 @@ console.log( // ] // } -await client.quit(); +client.destroy(); diff --git a/examples/search-knn.js b/examples/search-knn.js index ea20f52e3fe..49bd00d86df 100644 --- a/examples/search-knn.js +++ b/examples/search-knn.js @@ -88,4 +88,4 @@ console.log(JSON.stringify(results, null, 2)); // } // ] // } -await client.quit(); +client.destroy(); diff --git a/examples/set-scan.js b/examples/set-scan.js index 73f6c443444..0e379224d9d 100644 --- a/examples/set-scan.js +++ b/examples/set-scan.js @@ -12,4 +12,4 @@ for await (const member of client.sScanIterator(setName)) { console.log(member); } -await client.quit(); +client.destroy(); diff --git a/examples/sorted-set.js b/examples/sorted-set.js index eb1f82867c1..3fcc24b8442 100644 --- a/examples/sorted-set.js +++ b/examples/sorted-set.js @@ -28,4 +28,4 @@ for await (const memberWithScore of client.zScanIterator('mysortedset')) { console.log(memberWithScore); } -await client.quit(); +client.destroy(); diff --git a/examples/stream-producer.js b/examples/stream-producer.js index f81931e5197..113265dbd40 100644 --- a/examples/stream-producer.js +++ b/examples/stream-producer.js @@ -47,4 +47,4 @@ console.log(`Length of mystream: ${await client.xLen('mystream')}.`); // Should be approximately 1000: console.log(`Length of mytrimmedstream: ${await client.xLen('mytrimmedstream')}.`); -await client.quit(); +client.destroy(); diff --git a/examples/time-series.js b/examples/time-series.js index 2f2ac598032..1d61ff94408 100644 --- a/examples/time-series.js +++ b/examples/time-series.js @@ -119,4 +119,4 @@ try { console.error(e); } -await client.quit(); +client.destroy(); diff --git a/examples/topk.js b/examples/topk.js index 35cdc4a8500..d09144c230c 100644 --- a/examples/topk.js +++ b/examples/topk.js @@ -110,4 +110,4 @@ const [ simonCount, lanceCount ] = await client.topK.count('mytopk', [ console.log(`Count estimate for simon: ${simonCount}.`); console.log(`Count estimate for lance: ${lanceCount}.`); -await client.quit(); +client.destroy(); diff --git a/examples/transaction-with-arbitrary-commands.js b/examples/transaction-with-arbitrary-commands.js index 274a362d57e..d68533205a1 100644 --- a/examples/transaction-with-arbitrary-commands.js +++ b/examples/transaction-with-arbitrary-commands.js @@ -37,4 +37,4 @@ console.log(responses); // Clean up fixtures. await client.del(['hash1', 'hash2', 'hash3']); -await client.quit(); +client.destroy(); diff --git a/index.ts b/index.ts deleted file mode 100644 index 5b5a6e81294..00000000000 --- a/index.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { - RedisModules, - RedisFunctions, - RedisScripts, - createClient as _createClient, - RedisClientOptions, - RedisClientType as _RedisClientType, - createCluster as _createCluster, - RedisClusterOptions, - RedisClusterType as _RedisClusterType -} from '@redis/client'; -import RedisBloomModules from '@redis/bloom'; -import RedisGraph from '@redis/graph'; -import RedisJSON from '@redis/json'; -import RediSearch from '@redis/search'; -import RedisTimeSeries from '@redis/time-series'; - -export * from '@redis/client'; -export * from '@redis/bloom'; -export * from '@redis/graph'; -export * from '@redis/json'; -export * from '@redis/search'; -export * from '@redis/time-series'; - -const modules = { - ...RedisBloomModules, - graph: RedisGraph, - json: RedisJSON, - ft: RediSearch, - ts: RedisTimeSeries -}; - -export type RedisDefaultModules = typeof modules; - -export type RedisClientType< - M extends RedisModules = RedisDefaultModules, - F extends RedisFunctions = Record, - S extends RedisScripts = Record -> = _RedisClientType; - -export function createClient< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts ->( - options?: RedisClientOptions -): _RedisClientType { - return _createClient({ - ...options, - modules: { - ...modules, - ...(options?.modules as M) - } - }); -} - -export type RedisClusterType< - M extends RedisModules = RedisDefaultModules, - F extends RedisFunctions = Record, - S extends RedisScripts = Record -> = _RedisClusterType; - -export function createCluster< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts ->( - options: RedisClusterOptions -): RedisClusterType { - return _createCluster({ - ...options, - modules: { - ...modules, - ...(options?.modules as M) - } - }); -} diff --git a/package-lock.json b/package-lock.json index 18a7003947e..aefd0678434 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,38 +1,24 @@ { - "name": "redis", - "version": "4.7.0", + "name": "redis-monorepo", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "redis", - "version": "4.7.0", - "license": "MIT", + "name": "redis-monorepo", "workspaces": [ "./packages/*" ], - "dependencies": { - "@redis/bloom": "1.2.0", - "@redis/client": "1.6.0", - "@redis/graph": "1.1.1", - "@redis/json": "1.0.7", - "@redis/search": "1.2.0", - "@redis/time-series": "1.1.0" - }, "devDependencies": { - "@tsconfig/node14": "^14.1.0", - "gh-pages": "^6.0.0", - "release-it": "^16.1.5", - "typescript": "^5.2.2" - } - }, - "node_modules/@aashutoshrathi/word-wrap": { - "version": "1.2.6", - "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/mocha": "^10.0.6", + "@types/node": "^20.11.16", + "gh-pages": "^6.1.1", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "release-it": "^17.0.3", + "tsx": "^4.7.0", + "typedoc": "^0.25.7", + "typescript": "^5.3.3" } }, "node_modules/@ampproject/remapping": { @@ -49,12 +35,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.22.13", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", - "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.22.13", + "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" }, "engines": { @@ -111,32 +97,53 @@ "node": ">=0.8.0" } }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/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/compat-data": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.20.tgz", - "integrity": "sha512-BQYjKbpXjoXwFW5jGqiizJQQT/aC7pFm9Ok1OWssonuguICi264lbgMzRp2ZMmRSlfkX6DsWDDcsrctK8Rwfiw==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", + "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.20.tgz", - "integrity": "sha512-Y6jd1ahLubuYweD/zJH+vvOY141v4f9igNQAQ+MBgq9JlHS2iTsZKn1aMsb3vGccZsXI16VzTBw52Xx0DWmtnA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", + "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", - "@babel/helper-compilation-targets": "^7.22.15", - "@babel/helper-module-transforms": "^7.22.20", - "@babel/helpers": "^7.22.15", - "@babel/parser": "^7.22.16", - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.22.20", - "@babel/types": "^7.22.19", - "convert-source-map": "^1.7.0", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helpers": "^7.23.9", + "@babel/parser": "^7.23.9", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", @@ -150,13 +157,19 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, "node_modules/@babel/generator": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.15.tgz", - "integrity": "sha512-Zu9oWARBqeVOW0dZOjXc3JObrzuqothQ3y/n1kUtrjCoCPLkXUwMvOo/F/TCfoHMbWIFlWwpZtkZVb9ga4U2pA==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.22.15", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" @@ -166,14 +179,14 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz", - "integrity": "sha512-y6EEzULok0Qvz8yyLkCvVX+02ic+By2UdOhylwUOvOn9dvYc9mKICJuuU1n1XBI02YWsNsnrY1kc6DVbjcXbtw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.22.9", - "@babel/helper-validator-option": "^7.22.15", - "browserslist": "^4.21.9", + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -181,21 +194,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-compilation-targets/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/@babel/helper-compilation-targets/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/@babel/helper-environment-visitor": { "version": "7.22.20", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", @@ -206,13 +204,13 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", - "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.22.5", - "@babel/types": "^7.22.5" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" @@ -243,9 +241,9 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.20.tgz", - "integrity": "sha512-dLT7JVWIUUxKOs1UnJUBR3S70YK+pKX6AbJgB2vMIvEkZkrfJDbYDJesnPshtKV4LhDOR3Oc5YULeDizRek+5A==", + "version": "7.23.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", + "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", @@ -286,9 +284,9 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", - "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" @@ -304,32 +302,32 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.15.tgz", - "integrity": "sha512-bMn7RmyFjY/mdECUbgn9eoSY4vqvacUnS9i9vGAGttgFWesO6B4CYWA7XlpbWgBt71iv/hfbPlynohStqnu5hA==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.15.tgz", - "integrity": "sha512-7pAjK0aSdxOwR+CcYAqgWOGy5dcfvzsTIfFTb2odQqW47MDfv14UaJDY6eng8ylM2EaeKXdxaSWESbkmaQHTmw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", + "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dev": true, "dependencies": { - "@babel/template": "^7.22.15", - "@babel/traverse": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -390,10 +388,31 @@ "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": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "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.22.16", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.16.tgz", - "integrity": "sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", + "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -403,260 +422,530 @@ } }, "node_modules/@babel/template": { - "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", - "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", + "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/parser": "^7.22.15", - "@babel/types": "^7.22.15" + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.20.tgz", - "integrity": "sha512-eU260mPZbU7mZ0N+X10pxXhQFMGTeLb9eFS0mxehS8HZp9o1uSnFeWQuG1UPrlxgA7QoUzFhOnilHDp0AXCyHw==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", + "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.22.13", - "@babel/generator": "^7.22.15", + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", "@babel/helper-environment-visitor": "^7.22.20", - "@babel/helper-function-name": "^7.22.5", + "@babel/helper-function-name": "^7.23.0", "@babel/helper-hoist-variables": "^7.22.5", "@babel/helper-split-export-declaration": "^7.22.6", - "@babel/parser": "^7.22.16", - "@babel/types": "^7.22.19", - "debug": "^4.1.0", + "@babel/parser": "^7.23.9", + "@babel/types": "^7.23.9", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/traverse/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/@babel/types": { - "version": "7.22.19", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.19.tgz", - "integrity": "sha512-P7LAw/LbojPzkgp5oznjE6tQEIWbp4PkkfrZDINTro9zgBRtI324/EYsiSI7lhPbpIQ+DCeR2NNmMWANGGfZsg==", + "version": "7.23.9", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", + "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.22.5", - "@babel/helper-validator-identifier": "^7.22.19", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], "dev": true, - "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" - }, + "optional": true, + "os": [ + "aix" + ], "engines": { "node": ">=12" } }, - "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "eslint-visitor-keys": "^3.3.0" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "node": ">=12" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.8.1.tgz", - "integrity": "sha512-PWiOzLIUAjN/w5K17PoF4n6sKBw0gqLHPhywmYHP4t1VFQQVYeb1yWsJwnMVEMl3tUHME7X/SJPZLmtG7XBDxQ==", + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", - "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=12" } }, - "node_modules/@eslint/eslintrc/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==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], "dev": true, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=12" } }, - "node_modules/@eslint/js": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.49.0.tgz", - "integrity": "sha512-1S8uAY/MTJqVx0SC4epBq+N2yhuwtNwLbJYNZyhL2pO1ZVKn5HFXav5T41Ryzy9K9V7ZId2JB2oy/W4aCd9/2w==", + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], "dev": true, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=12" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.11", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.11.tgz", - "integrity": "sha512-N2brEuAadi0CcdeMXUkhbZB84eskAc8MEX1By6qEchoVywSgXPIjou4rYsl0V3Hj0ZnuGycGCjdNgockbzeWNA==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.5" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=10.10.0" + "node": ">=12" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "node": ">=12" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true - }, - "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==", + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], "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" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "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==", + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], "dev": true, - "dependencies": { - "sprintf-js": "~1.0.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" + "node": ">=12" } }, - "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==", + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], "dev": true, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/@istanbuljs/nyc-config-typescript": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", - "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], "dev": true, - "dependencies": { - "@istanbuljs/schema": "^0.1.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=8" - }, - "peerDependencies": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "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.2", + "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", + "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { "nyc": ">=15" } }, @@ -708,9 +997,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", - "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "version": "0.3.22", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", + "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -718,10 +1007,13 @@ } }, "node_modules/@ljharb/through": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.9.tgz", - "integrity": "sha512-yN599ZBuMPPK4tdoToLlvgJB4CLK8fGl7ntfy0Wn7U6ttNvHYurd81bfUiK/6sMkiIwm65R6ck4L6+Y3DfVbNQ==", + "version": "2.3.12", + "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", + "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", "dev": true, + "dependencies": { + "call-bind": "^1.0.5" + }, "engines": { "node": ">= 0.4" } @@ -762,194 +1054,158 @@ } }, "node_modules/@octokit/auth-token": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz", - "integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", + "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", "dev": true, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/core": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.4.tgz", - "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", + "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", "dev": true, "dependencies": { - "@octokit/auth-token": "^3.0.0", - "@octokit/graphql": "^5.0.0", - "@octokit/request": "^6.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", + "@octokit/auth-token": "^4.0.0", + "@octokit/graphql": "^7.0.0", + "@octokit/request": "^8.0.2", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", "before-after-hook": "^2.2.0", "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/endpoint": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", - "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", + "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", "dev": true, "dependencies": { - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", + "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/graphql": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", - "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", + "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", "dev": true, "dependencies": { - "@octokit/request": "^6.0.0", - "@octokit/types": "^9.0.0", + "@octokit/request": "^8.0.1", + "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/openapi-types": { - "version": "18.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.0.0.tgz", - "integrity": "sha512-V8GImKs3TeQRxRtXFpG2wl19V7444NIOTDF24AWuIbmNaNYOQMWRbjcGDXV5B+0n887fgDcuMNOmlul+k+oJtw==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", + "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==", "dev": true }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz", - "integrity": "sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==", + "version": "9.1.5", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", + "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", "dev": true, "dependencies": { - "@octokit/tsconfig": "^1.0.2", - "@octokit/types": "^9.2.3" + "@octokit/types": "^12.4.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=4" + "@octokit/core": ">=5" } }, "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==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.0.tgz", + "integrity": "sha512-2uJI1COtYCq8Z4yNSnM231TgH50bRkheQ9+aH8TnZanB6QilOnx8RMD2qsnamSOXtDj0ilxvevf5fGsBhBBzKA==", "dev": true, + "engines": { + "node": ">= 18" + }, "peerDependencies": { - "@octokit/core": ">=3" + "@octokit/core": ">=5" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz", - "integrity": "sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.2.0.tgz", + "integrity": "sha512-ePbgBMYtGoRNXDyKGvr9cyHjQ163PbwD0y1MkDJCpkO2YH4OeXX40c4wYHKikHGZcpGPbcRLuy0unPUuafco8Q==", "dev": true, "dependencies": { - "@octokit/types": "^10.0.0" + "@octokit/types": "^12.3.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" }, "peerDependencies": { - "@octokit/core": ">=3" - } - }, - "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz", - "integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==", - "dev": true, - "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "@octokit/core": ">=5" } }, "node_modules/@octokit/request": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", - "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", + "version": "8.1.6", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz", + "integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==", "dev": true, "dependencies": { - "@octokit/endpoint": "^7.0.0", - "@octokit/request-error": "^3.0.0", - "@octokit/types": "^9.0.0", - "is-plain-object": "^5.0.0", - "node-fetch": "^2.6.7", + "@octokit/endpoint": "^9.0.0", + "@octokit/request-error": "^5.0.0", + "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, "node_modules/@octokit/request-error": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", - "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", + "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", "dev": true, "dependencies": { - "@octokit/types": "^9.0.0", + "@octokit/types": "^12.0.0", "deprecation": "^2.0.0", "once": "^1.4.0" }, "engines": { - "node": ">= 14" - } - }, - "node_modules/@octokit/request/node_modules/node-fetch": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", - "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", - "dev": true, - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } + "node": ">= 18" } }, "node_modules/@octokit/rest": { - "version": "19.0.13", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.13.tgz", - "integrity": "sha512-/EzVox5V9gYGdbAI+ovYj3nXQT1TtTHRT+0eZPcuC05UFSWO3mdO9UY1C0i2eLF9Un1ONJkAk+IEtYGAC+TahA==", + "version": "20.0.2", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz", + "integrity": "sha512-Ux8NDgEraQ/DMAU1PlAohyfBBXDwhnX2j33Z1nJNziqAfHi70PuxkFYIcIt8aIAxtRE7KVuKp8lSR8pA0J5iOQ==", "dev": true, "dependencies": { - "@octokit/core": "^4.2.1", - "@octokit/plugin-paginate-rest": "^6.1.2", - "@octokit/plugin-request-log": "^1.0.4", - "@octokit/plugin-rest-endpoint-methods": "^7.1.2" + "@octokit/core": "^5.0.0", + "@octokit/plugin-paginate-rest": "^9.0.0", + "@octokit/plugin-request-log": "^4.0.0", + "@octokit/plugin-rest-endpoint-methods": "^10.0.0" }, "engines": { - "node": ">= 14" + "node": ">= 18" } }, - "node_modules/@octokit/tsconfig": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@octokit/tsconfig/-/tsconfig-1.0.2.tgz", - "integrity": "sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==", - "dev": true - }, "node_modules/@octokit/types": { - "version": "9.3.2", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", - "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", + "version": "12.4.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", + "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", "dev": true, "dependencies": { - "@octokit/openapi-types": "^18.0.0" + "@octokit/openapi-types": "^19.1.0" } }, "node_modules/@pnpm/config.env-replace": { @@ -1011,517 +1267,152 @@ }, "node_modules/@redis/search": { "resolved": "packages/search", - "link": true - }, - "node_modules/@redis/test-utils": { - "resolved": "packages/test-utils", - "link": true - }, - "node_modules/@redis/time-series": { - "resolved": "packages/time-series", - "link": true - }, - "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.0.tgz", - "integrity": "sha512-jXBtWAF4vmdNmZgD5FoKsVLv3rPgDnLgPbU84LIJ3otV44vJlDRokVng5v8NFJdCf/da9legHcKaRuZs4L7faA==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/fake-timers": { - "version": "10.3.0", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", - "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^3.0.0" - } - }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" - } - }, - "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", - "dev": true, - "dependencies": { - "type-detect": "4.0.8" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true - }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dev": true, - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@tootallnate/quickjs-emscripten": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true - }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true - }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true - }, - "node_modules/@tsconfig/node14": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-14.1.0.tgz", - "integrity": "sha512-VmsCG04YR58ciHBeJKBDNMWWfYbyP8FekWVuTlpstaUPlat1D0x/tXzkWP7yCMU0eSz9V4OZU0LBWTFJ3xZf6w==", - "dev": true - }, - "node_modules/@tsconfig/node16": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", - "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", - "dev": true - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.2.tgz", - "integrity": "sha512-FD+nQWA2zJjh4L9+pFXqWOi0Hs1ryBCfI+985NjluQ1p8EYtoLvjLOKidXBtZ4/IcxDX4o8/E8qDS3540tNliw==", - "dev": true - }, - "node_modules/@types/json-schema": { - "version": "7.0.13", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.13.tgz", - "integrity": "sha512-RbSSoHliUbnXj3ny0CNFOoxrIDV6SUGyStHsvDqosw6CkdPV8TtWGlfecuK4ToyMEAql6pzNxgCFKanovUzlgQ==", - "dev": true - }, - "node_modules/@types/mocha": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.1.tgz", - "integrity": "sha512-/fvYntiO1GeICvqbQ3doGDIP97vWmvFt83GKguJ6prmQM2iXZfFcq6YE8KteFyRtX2/h5Hf91BYvPodJKFYv5Q==", - "dev": true - }, - "node_modules/@types/node": { - "version": "20.6.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.2.tgz", - "integrity": "sha512-Y+/1vGBHV/cYk6OI1Na/LHzwnlNCAfU3ZNGrc1LdRe/LAIbdDPTTv/HU3M7yXN448aTVDq3eKRm2cg7iKLb8gw==", - "dev": true - }, - "node_modules/@types/semver": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.2.tgz", - "integrity": "sha512-7aqorHYgdNO4DM36stTiGO3DvKoex9TQRwsJU6vMaFGyqpBA1MNZkz+PG3gaNUPpTAOYhT1WR7M1JyA3fbS9Cw==", - "dev": true - }, - "node_modules/@types/sinon": { - "version": "10.0.16", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-10.0.16.tgz", - "integrity": "sha512-j2Du5SYpXZjJVJtXBokASpPRj+e2z+VUhCPHmM6WMfe3dpHu6iVKJMU6AiBcMp/XTAYnEj6Wc1trJUWwZ0QaAQ==", - "dev": true, - "dependencies": { - "@types/sinonjs__fake-timers": "*" - } - }, - "node_modules/@types/sinonjs__fake-timers": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.2.tgz", - "integrity": "sha512-9GcLXF0/v3t80caGs5p2rRfkB+a8VBGLJZVih6CNFkx8IZ994wiKKLSRs9nuFwk1HevWs/1mnUmkApGrSGsShA==", - "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/@types/yargs": { - "version": "17.0.24", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.24.tgz", - "integrity": "sha512-6i0aC7jV6QzQB8ne1joVZ0eSFIstHsCrobmOtghM11yGlH0j43FKL2UhWdELkyps0zuf7qVTUVCCR+tgSlyLLw==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", - "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", - "dev": true - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.7.2.tgz", - "integrity": "sha512-ooaHxlmSgZTM6CHYAFRlifqh1OAr3PAQEwi7lhYhaegbnXrnh7CDcHmc3+ihhbQC7H0i4JF0psI5ehzkF6Yl6Q==", - "dev": true, - "dependencies": { - "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.7.2", - "@typescript-eslint/type-utils": "6.7.2", - "@typescript-eslint/utils": "6.7.2", - "@typescript-eslint/visitor-keys": "6.7.2", - "debug": "^4.3.4", - "graphemer": "^1.4.0", - "ignore": "^5.2.4", - "natural-compare": "^1.4.0", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/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/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.7.2.tgz", - "integrity": "sha512-KA3E4ox0ws+SPyxQf9iSI25R6b4Ne78ORhNHeVKrPQnoYsb9UhieoiRoJgrzgEeKGOXhcY1i8YtOeCHHTDa6Fw==", - "dev": true, - "dependencies": { - "@typescript-eslint/scope-manager": "6.7.2", - "@typescript-eslint/types": "6.7.2", - "@typescript-eslint/typescript-estree": "6.7.2", - "@typescript-eslint/visitor-keys": "6.7.2", - "debug": "^4.3.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.7.2.tgz", - "integrity": "sha512-bgi6plgyZjEqapr7u2mhxGR6E8WCzKNUFWNh6fkpVe9+yzRZeYtDTbsIBzKbcxI+r1qVWt6VIoMSNZ4r2A+6Yw==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.7.2", - "@typescript-eslint/visitor-keys": "6.7.2" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } + "link": true }, - "node_modules/@typescript-eslint/type-utils": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.7.2.tgz", - "integrity": "sha512-36F4fOYIROYRl0qj95dYKx6kybddLtsbmPIYNK0OBeXv2j9L5nZ17j9jmfy+bIDHKQgn2EZX+cofsqi8NPATBQ==", - "dev": true, - "dependencies": { - "@typescript-eslint/typescript-estree": "6.7.2", - "@typescript-eslint/utils": "6.7.2", - "debug": "^4.3.4", - "ts-api-utils": "^1.0.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } + "node_modules/@redis/test-utils": { + "resolved": "packages/test-utils", + "link": true }, - "node_modules/@typescript-eslint/types": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.7.2.tgz", - "integrity": "sha512-flJYwMYgnUNDAN9/GAI3l8+wTmvTYdv64fcH8aoJK76Y+1FCZ08RtI5zDerM/FYT5DMkAc+19E4aLmd5KqdFyg==", - "dev": true, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } + "node_modules/@redis/time-series": { + "resolved": "packages/time-series", + "link": true }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.7.2.tgz", - "integrity": "sha512-kiJKVMLkoSciGyFU0TOY0fRxnp9qq1AzVOHNeN1+B9erKFCJ4Z8WdjAkKQPP+b1pWStGFqezMLltxO+308dJTQ==", + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", "dev": true, - "dependencies": { - "@typescript-eslint/types": "6.7.2", - "@typescript-eslint/visitor-keys": "6.7.2", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.5.4", - "ts-api-utils": "^1.0.1" - }, "engines": { - "node": "^16.0.0 || >=18.0.0" + "node": ">=14.16" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/typescript-estree/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" + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/@sindresorhus/merge-streams": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", + "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@typescript-eslint/typescript-estree/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==", + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" + "type-detect": "4.0.8" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/@sinonjs/fake-timers": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", + "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "@sinonjs/commons": "^3.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" } }, - "node_modules/@typescript-eslint/utils": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.7.2.tgz", - "integrity": "sha512-ZCcBJug/TS6fXRTsoTkgnsvyWSiXwMNiPzBUani7hDidBdj1779qwM1FIAmpH4lvlOZNF3EScsxxuGifjpLSWQ==", + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", "dev": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.12", - "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.7.2", - "@typescript-eslint/types": "6.7.2", - "@typescript-eslint/typescript-estree": "6.7.2", - "semver": "^7.5.4" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^7.0.0 || ^8.0.0" + "type-detect": "4.0.8" } }, - "node_modules/@typescript-eslint/utils/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==", + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "dev": true + }, + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", "dev": true, "dependencies": { - "yallist": "^4.0.0" + "defer-to-connect": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=14.16" } }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true + }, + "node_modules/@types/mocha": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", + "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.11.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", + "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", "dev": true, "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" + "undici-types": "~5.26.4" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.7.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.7.2.tgz", - "integrity": "sha512-uVw9VIMFBUTz8rIeaUT3fFe8xIUx8r4ywAdlQv1ifH+6acn/XF8Y6rwJ7XNmkNMDrTW+7+vxFFPIF40nJCVsMQ==", + "node_modules/@types/sinon": { + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", + "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.7.2", - "eslint-visitor-keys": "^3.4.1" - }, - "engines": { - "node": "^16.0.0 || >=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@types/sinonjs__fake-timers": "*" } }, - "node_modules/acorn": { - "version": "8.10.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", - "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } + "node_modules/@types/sinonjs__fake-timers": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", + "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", + "dev": true }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@types/yargs": { + "version": "17.0.32", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", + "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "@types/yargs-parser": "*" } }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true }, "node_modules/agent-base": { "version": "7.1.0", @@ -1548,22 +1439,6 @@ "node": ">=8" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/ansi-align": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", @@ -1597,6 +1472,18 @@ "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.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -1658,12 +1545,6 @@ "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", "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", @@ -1671,13 +1552,16 @@ "dev": true }, "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz", - "integrity": "sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -1724,17 +1608,18 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.2.tgz", - "integrity": "sha512-yMBKppFur/fbHu9/6USUe03bZ4knMYiwFBcyiaXB8Go0qNehwX6inYPzK9U0NeQvGxKthcmHcaR8P5MStSRBAw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "get-intrinsic": "^1.2.1", - "is-array-buffer": "^3.0.2", + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" }, "engines": { @@ -1757,9 +1642,9 @@ } }, "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", "dev": true }, "node_modules/async-retry": { @@ -1772,9 +1657,9 @@ } }, "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", + "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", "dev": true, "engines": { "node": ">= 0.4" @@ -1810,9 +1695,9 @@ ] }, "node_modules/basic-ftp": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.3.tgz", - "integrity": "sha512-QHX8HLlncOLpy54mh+k/sWIFd0ThmRqwe9ZjELybGZK+tZ8rUb9VO0saKJUROTbE+KhzDUT7xziGpGrW8Kmd+g==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.4.tgz", + "integrity": "sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==", "dev": true, "engines": { "node": ">=10.0.0" @@ -1824,15 +1709,6 @@ "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", "dev": true }, - "node_modules/big-integer": { - "version": "1.6.51", - "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz", - "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -1843,12 +1719,12 @@ } }, "node_modules/bl": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-5.1.0.tgz", - "integrity": "sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", "dev": true, "dependencies": { - "buffer": "^6.0.3", + "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } @@ -1899,6 +1775,30 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/boxen/node_modules/camelcase": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", + "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boxen/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/boxen/node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1966,18 +1866,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/bplist-parser": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.2.0.tgz", - "integrity": "sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==", - "dev": true, - "dependencies": { - "big-integer": "^1.6.44" - }, - "engines": { - "node": ">= 5.10.0" - } - }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -2007,9 +1895,9 @@ "dev": true }, "node_modules/browserslist": { - "version": "4.21.10", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.10.tgz", - "integrity": "sha512-bipEBdZfVH5/pwrvqc+Ub0kUPVfGUhlKxbvfD+z1BDnPEO/X98ruXGA1WP5ASpAFKan7Qr6j736IacbZQuAlKQ==", + "version": "4.22.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", + "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, "funding": [ { @@ -2026,10 +1914,10 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001517", - "electron-to-chromium": "^1.4.477", - "node-releases": "^2.0.13", - "update-browserslist-db": "^1.0.11" + "caniuse-lite": "^1.0.30001580", + "electron-to-chromium": "^1.4.648", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" }, "bin": { "browserslist": "cli.js" @@ -2039,9 +1927,9 @@ } }, "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", "dev": true, "funding": [ { @@ -2059,25 +1947,19 @@ ], "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.2.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/bundle-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-3.0.0.tgz", - "integrity": "sha512-PKA4BeSvBpQKQ8iPOGCSiell+N8P+Tf1DlwqmYhpe2gAhKPHn8EYOxVT+ShuGmhg8lN8XiSlS80yiExKXrURlw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, "dependencies": { - "run-applescript": "^5.0.0" + "run-applescript": "^7.0.0" }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2093,12 +1975,12 @@ } }, "node_modules/cacheable-request": { - "version": "10.2.13", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.13.tgz", - "integrity": "sha512-3SD4rrMu1msNGEtNSt8Od6enwdo//U9s4ykmXfA2TD58kcLkCobtCDiby7kNyj7a/Q7lz/mAesAFI54rTdnvBA==", + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", "dev": true, "dependencies": { - "@types/http-cache-semantics": "^4.0.1", + "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", @@ -2110,6 +1992,18 @@ "node": ">=14.16" } }, + "node_modules/cacheable-request/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/caching-transform": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/caching-transform/-/caching-transform-4.0.0.tgz", @@ -2126,13 +2020,14 @@ } }, "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==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2148,21 +2043,18 @@ } }, "node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001535", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001535.tgz", - "integrity": "sha512-48jLyUkiWFfhm/afF7cQPqPjaUmSraEhK4j+FCTJpgnGGEZHqyLe3hmWH7lIooZdSzXL0ReMvHz0vKDoTBsrwg==", + "version": "1.0.30001584", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001584.tgz", + "integrity": "sha512-LOz7CCQ9M1G7OjJOF9/mzmqmj3jE/7VOmrfw6Mgs0E8cjOsbRXQJHsPBfmBOXDskXKrHLyyW3n7kpDW/4BsfpQ==", "dev": true, "funding": [ { @@ -2180,17 +2072,33 @@ ] }, "node_modules/chalk": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "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": "^12.17.0 || ^14.13 || >=16.0.0" + "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", @@ -2225,9 +2133,9 @@ } }, "node_modules/ci-info": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", - "integrity": "sha512-eXTggHWSooYhq49F2opQhuHWgzucfF2YgODK4e1566GQs5BIfP30B0oenwBJHfWxAs2fyPB1s7Mg949zLf61Yw==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -2273,9 +2181,9 @@ } }, "node_modules/cli-spinners": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.1.tgz", - "integrity": "sha512-jHgecW0pxkonBJdrKsqxgRX9AcG+u/5k0Q7WPDfi8AogLAdwxEkyYYNWwZ5GvVFoFx2uiY1eNcSK00fh+1+FyQ==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, "engines": { "node": ">=6" @@ -2294,17 +2202,14 @@ } }, "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "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.1", + "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" } }, "node_modules/cliui/node_modules/wrap-ansi": { @@ -2360,9 +2265,9 @@ "dev": true }, "node_modules/commander": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", - "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "dev": true, "engines": { "node": ">=16" @@ -2422,29 +2327,31 @@ "dev": true }, "node_modules/cosmiconfig": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", - "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, "dependencies": { - "import-fresh": "^3.2.1", + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "engines": { "node": ">=14" }, "funding": { "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "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", @@ -2512,6 +2419,12 @@ } } }, + "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", @@ -2557,41 +2470,29 @@ "node": ">=4.0.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, "node_modules/default-browser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-4.0.0.tgz", - "integrity": "sha512-wX5pXO1+BrhMkSbROFsyxUm0i/cJEScyNhA4PPxc41ICuv05ZZB/MX28s8aZx6xjmatvebIapF6hLEKEcpneUA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", "dev": true, "dependencies": { - "bundle-name": "^3.0.0", - "default-browser-id": "^3.0.0", - "execa": "^7.1.1", - "titleize": "^3.0.0" + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/default-browser-id": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-3.0.0.tgz", - "integrity": "sha512-OZ1y3y0SqSICtE8DE4S8YOE9UZOJ8wO16fKWVP5J1Qz42kV9jcnMVFrEE/noXb/ss3Q4pZIH79kxofzyNNtUNA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", "dev": true, - "dependencies": { - "bplist-parser": "^0.2.0", - "untildify": "^4.0.0" - }, "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2634,9 +2535,9 @@ } }, "node_modules/define-data-property": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.0.tgz", - "integrity": "sha512-UzGwzcjyv3OtAvolTj1GoyNYzfFR+iqbGjcnBEENZVCpM4/Ng1yhGNvS3lR/xDS74Tb2wGG9WzNSNIOS9UVb2g==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "dev": true, "dependencies": { "get-intrinsic": "^1.2.1", @@ -2705,30 +2606,6 @@ "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/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -2751,9 +2628,9 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.523", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.523.tgz", - "integrity": "sha512-9AreocSUWnzNtvLcbpng6N+GkXnCcBR80IQkxRC9Dfdyg4gaWNUPBujAHUpKkiUkoSoR9UlhA4zD/IgBklmhzg==", + "version": "1.4.656", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.656.tgz", + "integrity": "sha512-9AQB5eFTHyR3Gvt2t/NwR0le2jBSUNwCnMbUCejFWHD+so4tH40/dRLgoE+jxlPeWS43XJewyvCv+I8LPMl49Q==", "dev": true }, "node_modules/email-addresses": { @@ -2768,6 +2645,15 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -2778,26 +2664,26 @@ } }, "node_modules/es-abstract": { - "version": "1.22.2", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.2.tgz", - "integrity": "sha512-YoxfFcDmhjOgWPWsV13+2RNjq1F6UQnfs+8TftwNqtzlmFzEXvlUwdrNrYeaizfjQzRMxkZ6ElWMOJIFKdVqwA==", + "version": "1.22.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", + "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.0", "arraybuffer.prototype.slice": "^1.0.2", "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "call-bind": "^1.0.5", "es-set-tostringtag": "^2.0.1", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.1", + "get-intrinsic": "^1.2.2", "get-symbol-description": "^1.0.0", "globalthis": "^1.0.3", "gopd": "^1.0.1", - "has": "^1.0.3", "has-property-descriptors": "^1.0.0", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", + "hasown": "^2.0.0", "internal-slot": "^1.0.5", "is-array-buffer": "^3.0.2", "is-callable": "^1.2.7", @@ -2807,7 +2693,7 @@ "is-string": "^1.0.7", "is-typed-array": "^1.1.12", "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", + "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.4", "regexp.prototype.flags": "^1.5.1", @@ -2821,7 +2707,7 @@ "typed-array-byte-offset": "^1.0.0", "typed-array-length": "^1.0.4", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.13" }, "engines": { "node": ">= 0.4" @@ -2836,6 +2722,15 @@ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", "dev": true }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-get-iterator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", @@ -2845,293 +2740,118 @@ "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.1.tgz", - "integrity": "sha512-g3OMbtlwY3QewlqAiMLI47KywjWZoEytKr8pf6iTC8uJq5bIAH52Z9pnQ8pVL6whrCto53JZDuUIsifGeLorTg==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escodegen": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", - "dev": true, - "dependencies": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2" - }, - "bin": { - "escodegen": "bin/escodegen.js", - "esgenerate": "bin/esgenerate.js" - }, - "engines": { - "node": ">=6.0" - }, - "optionalDependencies": { - "source-map": "~0.6.1" - } - }, - "node_modules/eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.49.0.tgz", - "integrity": "sha512-jw03ENfm6VJI0jA9U+8H5zfl5b+FvuU3YYvZRdZHOlU2ggJkxrlkJH4HcDrZpj6YwD8kuYqvQM8LyesoazrSOQ==", - "dev": true, - "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.2", - "@eslint/js": "8.49.0", - "@humanwhocodes/config-array": "^0.11.11", - "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint/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/eslint/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" + "is-arguments": "^1.1.1", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.7", + "isarray": "^2.0.5", + "stop-iteration-iterator": "^1.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint/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==", + "node_modules/es-set-tostringtag": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", + "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", "dev": true, "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "get-intrinsic": "^1.2.2", + "has-tostringtag": "^1.0.0", + "hasown": "^2.0.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 0.4" } }, - "node_modules/eslint/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, "dependencies": { - "is-glob": "^4.0.3" + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint/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==", + "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/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=8" + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" } }, - "node_modules/eslint/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==", + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true, - "dependencies": { - "p-locate": "^5.0.0" - }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, - "node_modules/eslint/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==", + "node_modules/escape-goat": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", + "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", "dev": true, - "dependencies": { - "yocto-queue": "^0.1.0" - }, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/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==", + "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, - "dependencies": { - "p-limit": "^3.0.2" - }, "engines": { "node": ">=10" }, @@ -3139,33 +2859,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/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==", + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, "dependencies": { - "has-flag": "^4.0.0" + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", - "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "optionalDependencies": { + "source-map": "~0.6.1" } }, "node_modules/esprima": { @@ -3181,30 +2893,6 @@ "node": ">=4" } }, - "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", - "dev": true, - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, "node_modules/estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", @@ -3224,28 +2912,52 @@ } }, "node_modules/execa": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", - "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, "dependencies": { "cross-spawn": "^7.0.3", - "get-stream": "^6.0.1", - "human-signals": "^4.3.0", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", - "signal-exit": "^3.0.7", + "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" }, "engines": { - "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + "node": ">=16.17" }, "funding": { "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/execa/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/external-editor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", @@ -3260,16 +2972,10 @@ "node": ">=4" } }, - "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==", - "dev": true - }, "node_modules/fast-glob": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", - "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -3282,22 +2988,10 @@ "node": ">=8.6.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true - }, "node_modules/fastq": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", - "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -3342,16 +3036,28 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" + "engines": { + "node": ">=12" }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/filename-reserved-regex": { @@ -3410,16 +3116,19 @@ } }, "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==", + "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": "^5.0.0", + "locate-path": "^6.0.0", "path-exists": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat": { @@ -3431,26 +3140,6 @@ "flat": "cli.js" } }, - "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", - "dev": true, - "dependencies": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -3515,9 +3204,9 @@ ] }, "node_modules/fs-extra": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.1.1.tgz", - "integrity": "sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, "dependencies": { "graceful-fs": "^4.2.0", @@ -3549,10 +3238,13 @@ } }, "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 + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.6", @@ -3581,14 +3273,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generic-pool": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", - "integrity": "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g==", - "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", @@ -3607,16 +3291,32 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/get-intrinsic": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", - "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.3.tgz", + "integrity": "sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", + "es-errors": "^1.0.0", + "function-bind": "^1.1.2", "has-proto": "^1.0.1", - "has-symbols": "^1.0.3" + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3632,12 +3332,12 @@ } }, "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==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3659,14 +3359,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", + "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/get-uri": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.1.tgz", - "integrity": "sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.2.tgz", + "integrity": "sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw==", "dev": true, "dependencies": { "basic-ftp": "^5.0.2", - "data-uri-to-buffer": "^5.0.1", + "data-uri-to-buffer": "^6.0.0", "debug": "^4.3.4", "fs-extra": "^8.1.0" }, @@ -3675,9 +3387,9 @@ } }, "node_modules/get-uri/node_modules/data-uri-to-buffer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz", - "integrity": "sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.1.tgz", + "integrity": "sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==", "dev": true, "engines": { "node": ">= 14" @@ -3716,9 +3428,9 @@ } }, "node_modules/gh-pages": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.0.0.tgz", - "integrity": "sha512-FXZWJRsvP/fK2HJGY+Di6FRNHvqFF6gOIELaopDjXXgjeOYSNURcuYwEO/6bwuq6koP5Lnkvnr5GViXzuOB89g==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.1.1.tgz", + "integrity": "sha512-upnohfjBwN5hBP9w2dPE7HO5JJTHzSGMV1JrLrHvNuqmjoYHg6TBrCcnEoorjG/e0ejbuvnwyKMdTyM40PEByw==", "dev": true, "dependencies": { "async": "^3.2.4", @@ -3748,9 +3460,9 @@ } }, "node_modules/git-url-parse": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-13.1.0.tgz", - "integrity": "sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-14.0.0.tgz", + "integrity": "sha512-NnLweV+2A4nCvn4U/m2AoYu0pPKlsmhK9cknG7IMwsjFY1S2jxM+mAhsDxyxfCIGfGaD+dozsyX4b6vkYc83yQ==", "dev": true, "dependencies": { "git-up": "^7.0.0" @@ -3788,46 +3500,28 @@ "node": ">= 6" } }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "dev": true, - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals": { - "version": "13.21.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", - "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "node_modules/global-dirs": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", + "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", "dev": true, "dependencies": { - "type-fest": "^0.20.2" + "ini": "2.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globals/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==", + "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": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=4" } }, "node_modules/globalthis": { @@ -3898,30 +3592,24 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/got/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/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, - "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-bigints": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", @@ -3932,21 +3620,21 @@ } }, "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "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": ">=4" + "node": ">=8" } }, "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.1" + "get-intrinsic": "^1.2.2" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3977,12 +3665,12 @@ } }, "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "has-symbols": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -3991,18 +3679,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-yarn": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", - "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/hasha": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/hasha/-/hasha-5.2.2.tgz", @@ -4019,25 +3695,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/hasha/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==", + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "function-bind": "^1.1.2" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hasha/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": ">= 0.4" } }, "node_modules/he": { @@ -4075,9 +3742,9 @@ } }, "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", + "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dev": true, "dependencies": { "quick-lru": "^5.1.1", @@ -4101,12 +3768,12 @@ } }, "node_modules/human-signals": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", - "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, "engines": { - "node": ">=14.18.0" + "node": ">=16.17.0" } }, "node_modules/iconv-lite": { @@ -4142,9 +3809,9 @@ ] }, "node_modules/ignore": { - "version": "5.2.4", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", - "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, "engines": { "node": ">= 4" @@ -4166,6 +3833,15 @@ "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-lazy": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", @@ -4219,12 +3895,12 @@ } }, "node_modules/inquirer": { - "version": "9.2.10", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.10.tgz", - "integrity": "sha512-tVVNFIXU8qNHoULiazz612GFl+yqNfjMTbLuViNJE/d860Qxrd3NMrse8dm40VUQLOQeULvaQF8lpAhvysjeyA==", + "version": "9.2.12", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", + "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", "dev": true, "dependencies": { - "@ljharb/through": "^2.3.9", + "@ljharb/through": "^2.3.11", "ansi-escapes": "^4.3.2", "chalk": "^5.3.0", "cli-cursor": "^3.1.0", @@ -4244,48 +3920,16 @@ "node": ">=14.18.0" } }, - "node_modules/inquirer/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/inquirer/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/inquirer/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==", + "node_modules/inquirer/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, "engines": { - "node": ">=8" + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/inquirer/node_modules/is-interactive": { @@ -4297,50 +3941,6 @@ "node": ">=8" } }, - "node_modules/inquirer/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/inquirer/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/inquirer/node_modules/log-symbols/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/inquirer/node_modules/ora": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", @@ -4393,13 +3993,13 @@ } }, "node_modules/internal-slot": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.5.tgz", - "integrity": "sha512-Y+R5hJrzs52QCG2laLn4udYVnxsfny9CpOhNhUvk/SSSVyF6T27FzRbF0sroPidSu3X8oEAkOn2K804mjpt6UQ==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", + "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", "dev": true, "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", + "get-intrinsic": "^1.2.2", + "hasown": "^2.0.0", "side-channel": "^1.0.4" }, "engines": { @@ -4438,14 +4038,16 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.2.tgz", - "integrity": "sha512-y+FyyR/w8vfIRq4eQcM1EYgSTnmHXPqaF+IgzgraytCFq5Xh8lllDVmAZolPJiZttZLeFSINPYMaEJ7/vWUa1w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4522,12 +4124,12 @@ } }, "node_modules/is-core-module": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz", - "integrity": "sha512-Z7dk6Qo8pOCp3l4tsX2C5ZVas4V+UxwQodwZhLopL91TX8UyyHEXafPcyoeeWuLrwzHcr3igO78wNLwHJHsMCQ==", + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", + "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -4593,6 +4195,21 @@ "node": ">=0.10.0" } }, + "node_modules/is-in-ci": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-0.1.0.tgz", + "integrity": "sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ==", + "dev": true, + "bin": { + "is-in-ci": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -4723,15 +4340,6 @@ "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-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -4779,12 +4387,12 @@ } }, "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "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": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4821,12 +4429,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.12.tgz", - "integrity": "sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.11" + "which-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -4842,12 +4450,12 @@ "dev": true }, "node_modules/is-unicode-supported": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "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": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4875,41 +4483,20 @@ } }, "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==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-wsl/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" + "is-inside-container": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-yarn-global": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", - "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", - "dev": true, - "engines": { - "node": ">=12" - } - }, "node_modules/isarray": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", @@ -4939,9 +4526,9 @@ } }, "node_modules/istanbul-lib-coverage": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", - "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, "engines": { "node": ">=8" @@ -5005,15 +4592,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-report/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/istanbul-lib-report/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -5068,6 +4646,12 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-report/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", @@ -5159,18 +4743,6 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5184,9 +4756,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", + "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", "dev": true }, "node_modules/jsonfile": { @@ -5202,15 +4774,15 @@ } }, "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==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", "dev": true }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, "dependencies": { "json-buffer": "3.0.1" @@ -5231,19 +4803,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -5251,15 +4810,18 @@ "dev": true }, "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==", + "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": "^4.1.0" + "p-locate": "^5.0.0" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/lodash": { @@ -5304,12 +4866,6 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "dev": true }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, "node_modules/lodash.uniqby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", @@ -5317,16 +4873,16 @@ "dev": true }, "node_modules/log-symbols": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-5.1.0.tgz", - "integrity": "sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA==", + "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": "^5.0.0", - "is-unicode-supported": "^1.1.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { - "node": ">=12" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -5345,12 +4901,12 @@ } }, "node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "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, - "engines": { - "node": ">=12" + "dependencies": { + "yallist": "^3.0.2" } }, "node_modules/lunr": { @@ -5386,12 +4942,6 @@ "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": "4.3.0", "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", @@ -5538,73 +5088,6 @@ "url": "https://opencollective.com/mochajs" } }, - "node_modules/mocha/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/mocha/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/mocha/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/mocha/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/mocha/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/mocha/node_modules/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", @@ -5637,58 +5120,6 @@ "node": "*" } }, - "node_modules/mocha/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/mocha/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/mocha/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/mocha/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/mocha/node_modules/minimatch": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", @@ -5705,122 +5136,15 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/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/mocha/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/mocha/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/mocha/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/mocha/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/mocha/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/mocha/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/mocha/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" + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" } }, "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==", + "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": { @@ -5844,12 +5168,6 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true - }, "node_modules/netmask": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", @@ -5887,25 +5205,16 @@ } }, "node_modules/nise": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.4.tgz", - "integrity": "sha512-8+Ib8rRJ4L0o3kfmyVCL7gzrohyDe0cMFTBa2d364yIrEGMEoetznKJx899YxjybU6bL9SQkYPSBBs1gyYs8Xg==", - "dev": true, - "dependencies": { - "@sinonjs/commons": "^2.0.0", - "@sinonjs/fake-timers": "^10.0.2", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - } - }, - "node_modules/nise/node_modules/@sinonjs/commons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", "dev": true, "dependencies": { - "type-detect": "4.0.8" + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" } }, "node_modules/node-domexception": { @@ -5958,9 +5267,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", - "integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, "node_modules/normalize-path": { @@ -5985,9 +5294,9 @@ } }, "node_modules/npm-run-path": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", - "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", + "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", "dev": true, "dependencies": { "path-key": "^4.0.0" @@ -6052,15 +5361,6 @@ "node": ">=8.9" } }, - "node_modules/nyc/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/nyc/node_modules/cliui": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", @@ -6072,11 +5372,54 @@ "wrap-ansi": "^6.2.0" } }, - "node_modules/nyc/node_modules/resolve-from": { + "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/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "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" } @@ -6132,9 +5475,9 @@ } }, "node_modules/object-inspect": { - "version": "1.12.3", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", - "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6150,13 +5493,13 @@ } }, "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" }, @@ -6192,58 +5535,41 @@ } }, "node_modules/open": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-9.1.0.tgz", - "integrity": "sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/open/-/open-10.0.3.tgz", + "integrity": "sha512-dtbI5oW7987hwC9qjJTyABldTaa19SuyJse1QboWv3b0qCcrrLNVDqBx1XgELAjh9QTVQaP/C5b1nhQebd1H2A==", "dev": true, "dependencies": { - "default-browser": "^4.0.0", + "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", - "is-wsl": "^2.2.0" + "is-wsl": "^3.1.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optionator": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", - "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", - "dev": true, - "dependencies": { - "@aashutoshrathi/word-wrap": "^1.2.3", - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/ora": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-7.0.1.tgz", - "integrity": "sha512-0TUxTiFJWv+JnjWm4o9yvuskpEJLXTcng8MJuKd+SzAzp2o+OP3HWqNhB4OdJRt1Vsd9/mR0oyaEYlOnL7XIRw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.0.1.tgz", + "integrity": "sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==", "dev": true, "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^4.0.0", - "cli-spinners": "^2.9.0", + "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", - "is-unicode-supported": "^1.3.0", - "log-symbols": "^5.1.0", - "stdin-discarder": "^0.1.0", - "string-width": "^6.1.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.1", + "string-width": "^7.0.0", "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6261,6 +5587,18 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/ora/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/ora/node_modules/cli-cursor": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", @@ -6277,11 +5615,51 @@ } }, "node_modules/ora/node_modules/emoji-regex": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.2.1.tgz", - "integrity": "sha512-97g6QgOk8zlDRdgq1WxwgTMgEWGVAQvB5Fdpgc1MkNy56la5SKP9GsMXKDOdqwn90/41a8yPwIGk1Y6WVbeMQA==", + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", + "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", "dev": true }, + "node_modules/ora/node_modules/is-unicode-supported": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz", + "integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ora/node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -6323,17 +5701,17 @@ } }, "node_modules/ora/node_modules/string-width": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-6.1.0.tgz", - "integrity": "sha512-k01swCJAgQmuADB0YIc+7TuatfNvTBVOoaUWJjTB9R4VJzR5vNWzf5t42ESVZFPS8xTySF7CAdV4t/aaIm3UnQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", + "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", "dev": true, "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^10.2.1", - "strip-ansi": "^7.0.1" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6389,30 +5767,33 @@ } }, "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==", + "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": { - "p-try": "^2.0.0" + "yocto-queue": "^0.1.0" }, "engines": { - "node": ">=6" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "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==", + "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": "^2.2.0" + "p-limit": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/p-map": { @@ -6502,6 +5883,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/package-json/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/package-json/node_modules/got": { "version": "12.6.1", "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", @@ -6554,6 +5947,12 @@ "node": ">=10" } }, + "node_modules/package-json/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6636,27 +6035,21 @@ "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-to-regexp/node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", + "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", "dev": true }, "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==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/picocolors": { @@ -6719,13 +6112,56 @@ "node": ">=8" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "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": ">= 0.8.0" + "node": ">=8" } }, "node_modules/process-on-spawn": { @@ -6741,16 +6177,16 @@ } }, "node_modules/promise.allsettled": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.6.tgz", - "integrity": "sha512-22wJUOD3zswWFqgwjNHa1965LvqTX87WPu/lreY2KSd7SVcERfuZ4GfUaOnJNnvtoIv2yXT/W00YIGMetXtFXg==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.7.tgz", + "integrity": "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==", "dev": true, "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "get-intrinsic": "^1.1.3", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1", "iterate-value": "^1.0.2" }, "engines": { @@ -6773,38 +6209,38 @@ "dev": true }, "node_modules/proxy-agent": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.0.tgz", - "integrity": "sha512-0LdR757eTj/JfuU7TL2YCuAZnxWXu3tkJbg4Oq3geW/qFNT/32T0sp2HnZ9O0lMR4q3vwAt0+xCA8SR0WAD0og==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", + "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", "dev": true, "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.2", "lru-cache": "^7.14.1", - "pac-proxy-agent": "^7.0.0", + "pac-proxy-agent": "^7.0.1", "proxy-from-env": "^1.1.0", - "socks-proxy-agent": "^8.0.1" + "socks-proxy-agent": "^8.0.2" }, "engines": { "node": ">= 14" } }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true - }, - "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", - "dev": true, - "engines": { - "node": ">=6" - } + "dev": true }, "node_modules/pupa": { "version": "3.1.0", @@ -6883,6 +6319,15 @@ "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": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -6921,6 +6366,10 @@ "node": ">= 0.10" } }, + "node_modules/redis": { + "resolved": "packages/redis", + "link": true + }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", @@ -6966,35 +6415,45 @@ } }, "node_modules/release-it": { - "version": "16.1.5", - "resolved": "https://registry.npmjs.org/release-it/-/release-it-16.1.5.tgz", - "integrity": "sha512-w/zCljPZBSYcCwR9fjDB1zaYwie1CAQganUrwNqjtXacXhrrsS5E6dDUNLcxm2ypu8GWAgZNMJfuBJqIO2E7fA==", + "version": "17.0.3", + "resolved": "https://registry.npmjs.org/release-it/-/release-it-17.0.3.tgz", + "integrity": "sha512-QjTCmvQm91pwLEbvavEs9jofHNe8thsb9Uimin+8DNSwFRdUd73p0Owy2PP/Dzh/EegRkKq/o+4Pn1xp8pC1og==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/webpro" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/webpro" + } + ], "dependencies": { "@iarna/toml": "2.2.5", - "@octokit/rest": "19.0.13", + "@octokit/rest": "20.0.2", "async-retry": "1.3.3", "chalk": "5.3.0", - "cosmiconfig": "8.2.0", - "execa": "7.2.0", - "git-url-parse": "13.1.0", - "globby": "13.2.2", + "cosmiconfig": "9.0.0", + "execa": "8.0.1", + "git-url-parse": "14.0.0", + "globby": "14.0.0", "got": "13.0.0", - "inquirer": "9.2.10", + "inquirer": "9.2.12", "is-ci": "3.0.1", "issue-parser": "6.0.0", "lodash": "4.17.21", "mime-types": "2.1.35", "new-github-release-url": "2.0.0", "node-fetch": "3.3.2", - "open": "9.1.0", - "ora": "7.0.1", + "open": "10.0.3", + "ora": "8.0.1", "os-name": "5.1.0", - "promise.allsettled": "1.0.6", - "proxy-agent": "6.3.0", + "promise.allsettled": "1.0.7", + "proxy-agent": "6.3.1", "semver": "7.5.4", "shelljs": "0.8.5", - "update-notifier": "6.0.2", + "update-notifier": "7.0.0", "url-join": "5.0.0", "wildcard-match": "5.1.2", "yargs-parser": "21.1.1" @@ -7003,23 +6462,36 @@ "release-it": "bin/release-it.js" }, "engines": { - "node": ">=16" + "node": ">=18" + } + }, + "node_modules/release-it/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, "node_modules/release-it/node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", + "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", "dev": true, "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", + "@sindresorhus/merge-streams": "^1.0.0", + "fast-glob": "^3.3.2", "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7052,6 +6524,21 @@ "node": ">=10" } }, + "node_modules/release-it/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/release-it/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/release-zalgo": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", @@ -7080,9 +6567,9 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.6", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.6.tgz", - "integrity": "sha512-njhxM7mV12JfufShqGy3Rz8j11RPdLy4xi15UurGJeoHLfJpVXKdh3ueuOqbYUcDZnffr6X739JBo5LzyahEsw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { "is-core-module": "^2.13.0", @@ -7103,12 +6590,21 @@ "dev": true }, "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==", + "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": ">=4" + "node": ">=8" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/responselike": { @@ -7198,109 +6694,17 @@ } }, "node_modules/run-applescript": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-5.0.0.tgz", - "integrity": "sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==", - "dev": true, - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/run-applescript/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/run-applescript/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/run-applescript/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/run-applescript/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/run-applescript/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/run-applescript/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, "engines": { - "node": ">=6" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/run-applescript/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/run-async": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -7343,13 +6747,13 @@ } }, "node_modules/safe-array-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.0.1.tgz", - "integrity": "sha512-6XbUAseYE2KtOuGueyeobCySj9L4+66Tn6KQMOPQJrAJEowYKW/YR/MGJZl7FdydUdaFu4LYyDZjxf4/Nmo23Q==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", + "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1", + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", "has-symbols": "^1.0.3", "isarray": "^2.0.5" }, @@ -7381,15 +6785,18 @@ ] }, "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", + "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", + "call-bind": "^1.0.5", + "get-intrinsic": "^1.2.2", "is-regex": "^1.1.4" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7451,6 +6858,12 @@ "node": ">=10" } }, + "node_modules/semver-diff/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, "node_modules/serialize-javascript": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", @@ -7466,6 +6879,22 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "dev": true }, + "node_modules/set-function-length": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", + "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.1", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.2", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/set-function-name": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", @@ -7519,9 +6948,9 @@ } }, "node_modules/shiki": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.4.tgz", - "integrity": "sha512-IXCRip2IQzKwxArNNq1S+On4KPML3Yyn8Zzs/xRgcgOWIr8ntIK3IKzjFPfjy/7kt9ZMjc+FItfqHRBg8b6tNQ==", + "version": "0.14.7", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", + "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", "dev": true, "dependencies": { "ansi-sequence-parser": "^1.1.0", @@ -7551,16 +6980,16 @@ "dev": true }, "node_modules/sinon": { - "version": "16.0.0", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-16.0.0.tgz", - "integrity": "sha512-B8AaZZm9CT5pqe4l4uWJztfD/mOTa7dL8Qo0W4+s+t74xECOgSZDDQCBjNgIK3+n4kyxQrSTv2V5ul8K25qkiQ==", + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", + "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0", - "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/fake-timers": "^11.2.2", "@sinonjs/samsam": "^8.0.0", "diff": "^5.1.0", - "nise": "^5.1.4", + "nise": "^5.1.5", "supports-color": "^7.2.0" }, "funding": { @@ -7577,15 +7006,6 @@ "node": ">=0.3.1" } }, - "node_modules/sinon/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/sinon/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7599,12 +7019,12 @@ } }, "node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, "engines": { - "node": ">=12" + "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7663,16 +7083,6 @@ "node": ">=0.10.0" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/spawn-wrap": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/spawn-wrap/-/spawn-wrap-2.0.0.tgz", @@ -7697,15 +7107,12 @@ "dev": true }, "node_modules/stdin-discarder": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.1.0.tgz", - "integrity": "sha512-xhV7w8S+bUwlPTb4bAOUQhv8/cSS5offJuX8GQGq32ONF0ZtDWKfkdomM3HMRA+LhX6um/FZ0COqlwsjD53LeQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, - "dependencies": { - "bl": "^5.0.0" - }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7825,12 +7232,15 @@ } }, "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": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "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": ">=0.10.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/strip-outer": { @@ -7855,15 +7265,18 @@ } }, "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==", + "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": "^3.0.0" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=4" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, "node_modules/supports-preserve-symlinks-flag": { @@ -7892,24 +7305,6 @@ "node": ">=8" } }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/titleize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", - "integrity": "sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -7939,105 +7334,29 @@ "dependencies": { "is-number": "^7.0.0" }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true - }, - "node_modules/trim-repeated": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/trim-repeated/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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/ts-api-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.3.tgz", - "integrity": "sha512-wNMeqtMz5NtwpT/UZGY5alT+VoKdSsOOP/kqHFcUW1P/VRhH2wJ48+DN2WwUliNbQ976ETwDL0Ifd2VVvgonvg==", - "dev": true, - "engines": { - "node": ">=16.13.0" - }, - "peerDependencies": { - "typescript": ">=4.2.0" + "engines": { + "node": ">=8.0" } }, - "node_modules/ts-node": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", - "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "node_modules/trim-repeated": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", + "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", "dev": true, "dependencies": { - "@cspotcode/source-map-support": "^0.8.0", - "@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", - "v8-compile-cache-lib": "^3.0.1", - "yn": "3.1.1" - }, - "bin": { - "ts-node": "dist/bin.js", - "ts-node-cwd": "dist/bin-cwd.js", - "ts-node-esm": "dist/bin-esm.js", - "ts-node-script": "dist/bin-script.js", - "ts-node-transpile-only": "dist/bin-transpile.js", - "ts-script": "dist/bin-script-deprecated.js" - }, - "peerDependencies": { - "@swc/core": ">=1.2.50", - "@swc/wasm": ">=1.2.50", - "@types/node": "*", - "typescript": ">=2.7" + "escape-string-regexp": "^1.0.2" }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "@swc/wasm": { - "optional": true - } + "engines": { + "node": ">=0.10.0" } }, - "node_modules/ts-node/node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", - "dev": 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==", + "node_modules/trim-repeated/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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, "engines": { - "node": ">=0.3.1" + "node": ">=0.8.0" } }, "node_modules/tslib": { @@ -8046,16 +7365,23 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", "dev": true }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "node_modules/tsx": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.0.tgz", + "integrity": "sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==", "dev": true, "dependencies": { - "prelude-ls": "^1.2.1" + "esbuild": "~0.19.10", + "get-tsconfig": "^4.7.2" + }, + "bin": { + "tsx": "dist/cli.mjs" }, "engines": { - "node": ">= 0.8.0" + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" } }, "node_modules/type-detect": { @@ -8068,15 +7394,12 @@ } }, "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==", + "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": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" } }, "node_modules/typed-array-buffer": { @@ -8154,15 +7477,15 @@ } }, "node_modules/typedoc": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.1.tgz", - "integrity": "sha512-c2ye3YUtGIadxN2O6YwPEXgrZcvhlZ6HlhWZ8jQRNzwLPn2ylhdGqdR8HbyDRyALP8J6lmSANILCkkIdNPFxqA==", + "version": "0.25.7", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.7.tgz", + "integrity": "sha512-m6A6JjQRg39p2ZVRIN3NKXgrN8vzlHhOS+r9ymUYtcUP/TIQPvWSq7YgE5ZjASfv5Vd5BW5xrir6Gm2XNNcOow==", "dev": true, "dependencies": { "lunr": "^2.3.9", "marked": "^4.3.0", "minimatch": "^9.0.3", - "shiki": "^0.14.1" + "shiki": "^0.14.7" }, "bin": { "typedoc": "bin/typedoc" @@ -8171,7 +7494,7 @@ "node": ">= 16" }, "peerDependencies": { - "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x" + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x || 5.2.x || 5.3.x" } }, "node_modules/typedoc/node_modules/brace-expansion": { @@ -8199,9 +7522,9 @@ } }, "node_modules/typescript": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz", - "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -8226,6 +7549,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", @@ -8242,33 +7583,24 @@ } }, "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==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", "dev": true }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, "engines": { "node": ">= 10.0.0" } }, - "node_modules/untildify": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/untildify/-/untildify-4.0.0.tgz", - "integrity": "sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/update-browserslist-db": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", - "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", + "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -8296,33 +7628,43 @@ } }, "node_modules/update-notifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", - "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.0.0.tgz", + "integrity": "sha512-Hv25Bh+eAbOLlsjJreVPOs4vd51rrtCrmhyOJtbpAojro34jS4KQaEp4/EvlHJX7jSO42VvEFpkastVyXyIsdQ==", "dev": true, "dependencies": { - "boxen": "^7.0.0", - "chalk": "^5.0.1", + "boxen": "^7.1.1", + "chalk": "^5.3.0", "configstore": "^6.0.0", - "has-yarn": "^3.0.0", "import-lazy": "^4.0.0", - "is-ci": "^3.0.1", + "is-in-ci": "^0.1.0", "is-installed-globally": "^0.4.0", "is-npm": "^6.0.0", - "is-yarn-global": "^0.4.0", "latest-version": "^7.0.0", "pupa": "^3.1.0", - "semver": "^7.3.7", + "semver": "^7.5.4", "semver-diff": "^4.0.0", "xdg-basedir": "^5.1.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/yeoman/update-notifier?sponsor=1" } }, + "node_modules/update-notifier/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "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", @@ -8350,14 +7692,11 @@ "node": ">=10" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } + "node_modules/update-notifier/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, "node_modules/url-join": { "version": "5.0.0", @@ -8383,12 +7722,6 @@ "uuid": "dist/bin/uuid" } }, - "node_modules/v8-compile-cache-lib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", - "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", - "dev": true - }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", @@ -8411,30 +7744,14 @@ } }, "node_modules/web-streams-polyfill": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", - "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", + "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", "dev": true, "engines": { "node": ">= 8" } }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true - }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8473,16 +7790,16 @@ "dev": true }, "node_modules/which-typed-array": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.11.tgz", - "integrity": "sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==", + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", + "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", + "available-typed-arrays": "^1.0.6", + "call-bind": "^1.0.5", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" + "has-tostringtag": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -8600,6 +7917,18 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/windows-release/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/windows-release/node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -8609,18 +7938,6 @@ "node": ">=10.17.0" } }, - "node_modules/windows-release/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/windows-release/node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -8726,35 +8043,36 @@ } }, "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true }, "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", "dev": true, "dependencies": { - "cliui": "^8.0.1", + "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.3", + "string-width": "^4.2.0", "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" + "yargs-parser": "^20.2.2" }, "engines": { - "node": ">=12" + "node": ">=10" } }, "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "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": ">=12" + "node": ">=10" } }, "node_modules/yargs-unparser": { @@ -8796,15 +8114,6 @@ "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", @@ -8819,145 +8128,171 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "1.2.0", + "version": "2.0.0-next.3", "license": "MIT", "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" } }, "packages/client": { "name": "@redis/client", - "version": "1.6.0", + "version": "2.0.0-next.4", "license": "MIT", "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" + "cluster-key-slot": "1.1.2" }, "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "@types/sinon": "^10.0.16", - "@types/yallist": "^4.0.1", - "@typescript-eslint/eslint-plugin": "^6.7.2", - "@typescript-eslint/parser": "^6.7.2", - "eslint": "^8.49.0", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "sinon": "^16.0.0", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@types/sinon": "^17.0.3", + "sinon": "^17.0.1" }, "engines": { - "node": ">=14" + "node": ">= 18" } }, "packages/graph": { "name": "@redis/graph", - "version": "1.1.1", + "version": "2.0.0-next.2", "license": "MIT", "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" } }, "packages/json": { "name": "@redis/json", - "version": "1.0.7", + "version": "2.0.0-next.2", "license": "MIT", "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" + } + }, + "packages/redis": { + "version": "5.0.0-next.4", + "license": "MIT", + "dependencies": { + "@redis/bloom": "2.0.0-next.3", + "@redis/client": "2.0.0-next.4", + "@redis/graph": "2.0.0-next.2", + "@redis/json": "2.0.0-next.2", + "@redis/search": "2.0.0-next.2", + "@redis/time-series": "2.0.0-next.2" + }, + "engines": { + "node": ">= 18" } }, "packages/search": { "name": "@redis/search", - "version": "1.2.0", + "version": "2.0.0-next.2", "license": "MIT", "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" } }, "packages/test-utils": { "name": "@redis/test-utils", "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@types/mocha": "^10.0.1", - "@types/node": "^20.6.2", - "@types/yargs": "^17.0.24", - "mocha": "^10.2.0", - "nyc": "^15.1.0", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typescript": "^5.2.2", + "@types/yargs": "^17.0.32", "yargs": "^17.7.2" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "*" + } + }, + "packages/test-utils/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "packages/test-utils/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" + } + }, + "packages/test-utils/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "packages/test-utils/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" } }, "packages/time-series": { "name": "@redis/time-series", - "version": "1.1.0", + "version": "2.0.0-next.2", "license": "MIT", "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" } } } diff --git a/package.json b/package.json index e8ceef7173d..c626d4a48e8 100644 --- a/package.json +++ b/package.json @@ -1,50 +1,25 @@ { - "name": "redis", - "description": "A modern, high performance Redis client", - "version": "4.7.0", - "license": "MIT", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist/" - ], + "name": "redis-monorepo", + "private": true, "workspaces": [ "./packages/*" ], "scripts": { "test": "npm run test -ws --if-present", - "build:client": "npm run build -w ./packages/client", - "build:test-utils": "npm run build -w ./packages/test-utils", - "build:tests-tools": "npm run build:client && npm run build:test-utils", - "build:modules": "find ./packages -mindepth 1 -maxdepth 1 -type d ! -name 'client' ! -name 'test-utils' -exec npm run build -w {} \\;", - "build": "tsc", - "build-all": "npm run build:client && npm run build:test-utils && npm run build:modules && npm run build", - "documentation": "npm run documentation -ws --if-present", + "build": "tsc --build", + "documentation": "typedoc", "gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot '" }, - "dependencies": { - "@redis/bloom": "1.2.0", - "@redis/client": "1.6.0", - "@redis/graph": "1.1.1", - "@redis/json": "1.0.7", - "@redis/search": "1.2.0", - "@redis/time-series": "1.1.0" - }, "devDependencies": { - "@tsconfig/node14": "^14.1.0", - "gh-pages": "^6.0.0", - "release-it": "^16.1.5", - "typescript": "^5.2.2" - }, - "repository": { - "type": "git", - "url": "git://github.com/redis/node-redis.git" - }, - "bugs": { - "url": "https://github.com/redis/node-redis/issues" - }, - "homepage": "https://github.com/redis/node-redis", - "keywords": [ - "redis" - ] + "@istanbuljs/nyc-config-typescript": "^1.0.2", + "@types/mocha": "^10.0.6", + "@types/node": "^20.11.16", + "gh-pages": "^6.1.1", + "mocha": "^10.2.0", + "nyc": "^15.1.0", + "release-it": "^17.0.3", + "tsx": "^4.7.0", + "typedoc": "^0.25.7", + "typescript": "^5.3.3" + } } diff --git a/packages/bloom/.npmignore b/packages/bloom/.npmignore deleted file mode 100644 index bbef2b404fb..00000000000 --- a/packages/bloom/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -.nyc_output/ -coverage/ -lib/ -.nycrc.json -.release-it.json -tsconfig.json diff --git a/packages/bloom/.release-it.json b/packages/bloom/.release-it.json index 5d11263645f..3a27a088058 100644 --- a/packages/bloom/.release-it.json +++ b/packages/bloom/.release-it.json @@ -5,6 +5,7 @@ "tagAnnotation": "Release ${tagName}" }, "npm": { + "versionArgs": ["--workspaces-update=false"], "publishArgs": ["--access", "public"] } } diff --git a/packages/bloom/README.md b/packages/bloom/README.md index 8eb1445d188..e527ff5552c 100644 --- a/packages/bloom/README.md +++ b/packages/bloom/README.md @@ -1,14 +1,17 @@ # @redis/bloom -This package provides support for the [RedisBloom](https://redisbloom.io) module, which adds additional probabilistic data structures to Redis. It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RediBloom commands. +This package provides support for the [RedisBloom](https://redis.io/docs/data-types/probabilistic/) module, which adds additional probabilistic data structures to Redis. -To use these extra commands, your Redis server must have the RedisBloom module installed. +Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis). + +:warning: To use these extra commands, your Redis server must have the RedisBloom module installed. RedisBloom provides the following probabilistic data structures: * Bloom Filter: for checking set membership with a high degree of certainty. * Cuckoo Filter: for checking set membership with a high degree of certainty. -* Count-Min Sketch: Determine the frequency of events in a stream. +* T-Digest: for estimating the quantiles of a stream of data. * Top-K: Maintain a list of k most frequently seen items. +* Count-Min Sketch: Determine the frequency of events in a stream. -For complete examples, see `bloom-filter.js`, `cuckoo-filter.js`, `count-min-sketch.js` and `topk.js` in the Node Redis examples folder. +For some examples, see [`bloom-filter.js`](https://github.com/redis/node-redis/tree/master/examples/bloom-filter.js), [`cuckoo-filter.js`](https://github.com/redis/node-redis/tree/master/examples/cuckoo-filter.js), [`count-min-sketch.js`](https://github.com/redis/node-redis/tree/master/examples/count-min-sketch.js) and [`topk.js`](https://github.com/redis/node-redis/tree/master/examples/topk.js) in the [examples folder](https://github.com/redis/node-redis/tree/master/examples). diff --git a/packages/bloom/lib/commands/bloom/ADD.spec.ts b/packages/bloom/lib/commands/bloom/ADD.spec.ts index e7ec3409136..11267e2afdb 100644 --- a/packages/bloom/lib/commands/bloom/ADD.spec.ts +++ b/packages/bloom/lib/commands/bloom/ADD.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './ADD'; +import ADD from './ADD'; -describe('BF ADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['BF.ADD', 'key', 'item'] - ); - }); +describe('BF.ADD', () => { + it('transformArguments', () => { + assert.deepEqual( + ADD.transformArguments('key', 'item'), + ['BF.ADD', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.bf.add', async client => { - assert.equal( - await client.bf.add('key', 'item'), - true - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.bf.add', async client => { + assert.equal( + await client.bf.add('key', 'item'), + true + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/bloom/ADD.ts b/packages/bloom/lib/commands/bloom/ADD.ts index d8938f4c2b0..a9655754897 100644 --- a/packages/bloom/lib/commands/bloom/ADD.ts +++ b/packages/bloom/lib/commands/bloom/ADD.ts @@ -1,7 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export function transformArguments(key: string, item: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, item: RedisArgument) { return ['BF.ADD', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/CARD.spec.ts b/packages/bloom/lib/commands/bloom/CARD.spec.ts index 4d5620ea196..b150f812574 100644 --- a/packages/bloom/lib/commands/bloom/CARD.spec.ts +++ b/packages/bloom/lib/commands/bloom/CARD.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './CARD'; +import CARD from './CARD'; -describe('BF CARD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('bloom'), - ['BF.CARD', 'bloom'] - ); - }); +describe('BF.CARD', () => { + it('transformArguments', () => { + assert.deepEqual( + CARD.transformArguments('bloom'), + ['BF.CARD', 'bloom'] + ); + }); - testUtils.testWithClient('client.bf.card', async client => { - assert.equal( - await client.bf.card('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.bf.card', async client => { + assert.equal( + await client.bf.card('key'), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/bloom/CARD.ts b/packages/bloom/lib/commands/bloom/CARD.ts index 530284c3f60..ddaa76cc1f8 100644 --- a/packages/bloom/lib/commands/bloom/CARD.ts +++ b/packages/bloom/lib/commands/bloom/CARD.ts @@ -1,9 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['BF.CARD', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/EXISTS.spec.ts b/packages/bloom/lib/commands/bloom/EXISTS.spec.ts index 1088e739e61..7db891b92bf 100644 --- a/packages/bloom/lib/commands/bloom/EXISTS.spec.ts +++ b/packages/bloom/lib/commands/bloom/EXISTS.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './EXISTS'; +import EXISTS from './EXISTS'; -describe('BF EXISTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['BF.EXISTS', 'key', 'item'] - ); - }); +describe('BF.EXISTS', () => { + it('transformArguments', () => { + assert.deepEqual( + EXISTS.transformArguments('key', 'item'), + ['BF.EXISTS', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.bf.exists', async client => { - assert.equal( - await client.bf.exists('key', 'item'), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.bf.exists', async client => { + assert.equal( + await client.bf.exists('key', 'item'), + false + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/bloom/EXISTS.ts b/packages/bloom/lib/commands/bloom/EXISTS.ts index d044207e244..9d28d671d61 100644 --- a/packages/bloom/lib/commands/bloom/EXISTS.ts +++ b/packages/bloom/lib/commands/bloom/EXISTS.ts @@ -1,9 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string, item: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, item: RedisArgument) { return ['BF.EXISTS', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/INFO.spec.ts b/packages/bloom/lib/commands/bloom/INFO.spec.ts index 7a5e5724c22..4a17dab8d33 100644 --- a/packages/bloom/lib/commands/bloom/INFO.spec.ts +++ b/packages/bloom/lib/commands/bloom/INFO.spec.ts @@ -1,24 +1,26 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INFO'; +import INFO from './INFO'; -describe('BF INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('bloom'), - ['BF.INFO', 'bloom'] - ); - }); +describe('BF.INFO', () => { + it('transformArguments', () => { + assert.deepEqual( + INFO.transformArguments('bloom'), + ['BF.INFO', 'bloom'] + ); + }); - testUtils.testWithClient('client.bf.info', async client => { - await client.bf.reserve('key', 0.01, 100); + testUtils.testWithClient('client.bf.info', async client => { + const [, reply] = await Promise.all([ + client.bf.reserve('key', 0.01, 100), + client.bf.info('key') + ]); - const info = await client.bf.info('key'); - assert.equal(typeof info, 'object'); - assert.equal(info.capacity, 100); - assert.equal(typeof info.size, 'number'); - assert.equal(typeof info.numberOfFilters, 'number'); - assert.equal(typeof info.numberOfInsertedItems, 'number'); - assert.equal(typeof info.expansionRate, 'number'); - }, GLOBAL.SERVERS.OPEN); + assert.equal(typeof reply, 'object'); + assert.equal(reply['Capacity'], 100); + assert.equal(typeof reply['Size'], 'number'); + assert.equal(typeof reply['Number of filters'], 'number'); + assert.equal(typeof reply['Number of items inserted'], 'number'); + assert.equal(typeof reply['Expansion rate'], 'number'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/bloom/INFO.ts b/packages/bloom/lib/commands/bloom/INFO.ts index 52e97646404..208c999b970 100644 --- a/packages/bloom/lib/commands/bloom/INFO.ts +++ b/packages/bloom/lib/commands/bloom/INFO.ts @@ -1,38 +1,32 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command, UnwrapReply, NullReply, NumberReply, TuplesToMapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { transformInfoV2Reply } from '.'; -export const IS_READ_ONLY = true; +export type BfInfoReplyMap = TuplesToMapReply<[ + [SimpleStringReply<'Capacity'>, NumberReply], + [SimpleStringReply<'Size'>, NumberReply], + [SimpleStringReply<'Number of filters'>, NumberReply], + [SimpleStringReply<'Number of items inserted'>, NumberReply], + [SimpleStringReply<'Expansion rate'>, NullReply | NumberReply] +]>; -export function transformArguments(key: string): Array { - return ['BF.INFO', key]; -} - -export type InfoRawReply = [ - _: string, - capacity: number, - _: string, - size: number, - _: string, - numberOfFilters: number, - _: string, - numberOfInsertedItems: number, - _: string, - expansionRate: number, -]; - -export interface InfoReply { - capacity: number; - size: number; - numberOfFilters: number; - numberOfInsertedItems: number; - expansionRate: number; +export interface BfInfoReply { + capacity: NumberReply; + size: NumberReply; + numberOfFilters: NumberReply; + numberOfInsertedItems: NumberReply; + expansionRate: NullReply | NumberReply; } -export function transformReply(reply: InfoRawReply): InfoReply { - return { - capacity: reply[1], - size: reply[3], - numberOfFilters: reply[5], - numberOfInsertedItems: reply[7], - expansionRate: reply[9] - }; -} +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { + return ['BF.INFO', key]; + }, + transformReply: { + 2: (reply: UnwrapReply>, _, typeMapping?: TypeMapping): BfInfoReplyMap => { + return transformInfoV2Reply(reply, typeMapping); + }, + 3: undefined as unknown as () => BfInfoReplyMap + } +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/INSERT.spec.ts b/packages/bloom/lib/commands/bloom/INSERT.spec.ts index aff9e6e282b..ccd81e070f1 100644 --- a/packages/bloom/lib/commands/bloom/INSERT.spec.ts +++ b/packages/bloom/lib/commands/bloom/INSERT.spec.ts @@ -1,69 +1,69 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INSERT'; +import INSERT from './INSERT'; -describe('BF INSERT', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['BF.INSERT', 'key', 'ITEMS', 'item'] - ); - }); +describe('BF.INSERT', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + INSERT.transformArguments('key', 'item'), + ['BF.INSERT', 'key', 'ITEMS', 'item'] + ); + }); - it('with CAPACITY', () => { - assert.deepEqual( - transformArguments('key', 'item', { CAPACITY: 100 }), - ['BF.INSERT', 'key', 'CAPACITY', '100', 'ITEMS', 'item'] - ); - }); + it('with CAPACITY', () => { + assert.deepEqual( + INSERT.transformArguments('key', 'item', { CAPACITY: 100 }), + ['BF.INSERT', 'key', 'CAPACITY', '100', 'ITEMS', 'item'] + ); + }); - it('with ERROR', () => { - assert.deepEqual( - transformArguments('key', 'item', { ERROR: 0.01 }), - ['BF.INSERT', 'key', 'ERROR', '0.01', 'ITEMS', 'item'] - ); - }); + it('with ERROR', () => { + assert.deepEqual( + INSERT.transformArguments('key', 'item', { ERROR: 0.01 }), + ['BF.INSERT', 'key', 'ERROR', '0.01', 'ITEMS', 'item'] + ); + }); - it('with EXPANSION', () => { - assert.deepEqual( - transformArguments('key', 'item', { EXPANSION: 1 }), - ['BF.INSERT', 'key', 'EXPANSION', '1', 'ITEMS', 'item'] - ); - }); + it('with EXPANSION', () => { + assert.deepEqual( + INSERT.transformArguments('key', 'item', { EXPANSION: 1 }), + ['BF.INSERT', 'key', 'EXPANSION', '1', 'ITEMS', 'item'] + ); + }); - it('with NOCREATE', () => { - assert.deepEqual( - transformArguments('key', 'item', { NOCREATE: true }), - ['BF.INSERT', 'key', 'NOCREATE', 'ITEMS', 'item'] - ); - }); + it('with NOCREATE', () => { + assert.deepEqual( + INSERT.transformArguments('key', 'item', { NOCREATE: true }), + ['BF.INSERT', 'key', 'NOCREATE', 'ITEMS', 'item'] + ); + }); - it('with NONSCALING', () => { - assert.deepEqual( - transformArguments('key', 'item', { NONSCALING: true }), - ['BF.INSERT', 'key', 'NONSCALING', 'ITEMS', 'item'] - ); - }); + it('with NONSCALING', () => { + assert.deepEqual( + INSERT.transformArguments('key', 'item', { NONSCALING: true }), + ['BF.INSERT', 'key', 'NONSCALING', 'ITEMS', 'item'] + ); + }); - it('with CAPACITY, ERROR, EXPANSION, NOCREATE and NONSCALING', () => { - assert.deepEqual( - transformArguments('key', 'item', { - CAPACITY: 100, - ERROR: 0.01, - EXPANSION: 1, - NOCREATE: true, - NONSCALING: true - }), - ['BF.INSERT', 'key', 'CAPACITY', '100', 'ERROR', '0.01', 'EXPANSION', '1', 'NOCREATE', 'NONSCALING', 'ITEMS', 'item'] - ); - }); + it('with CAPACITY, ERROR, EXPANSION, NOCREATE and NONSCALING', () => { + assert.deepEqual( + INSERT.transformArguments('key', 'item', { + CAPACITY: 100, + ERROR: 0.01, + EXPANSION: 1, + NOCREATE: true, + NONSCALING: true + }), + ['BF.INSERT', 'key', 'CAPACITY', '100', 'ERROR', '0.01', 'EXPANSION', '1', 'NOCREATE', 'NONSCALING', 'ITEMS', 'item'] + ); }); + }); - testUtils.testWithClient('client.bf.insert', async client => { - assert.deepEqual( - await client.bf.insert('key', 'item'), - [true] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.bf.insert', async client => { + assert.deepEqual( + await client.bf.insert('key', 'item'), + [true] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/bloom/INSERT.ts b/packages/bloom/lib/commands/bloom/INSERT.ts index f6deb7a8612..dfeaf5f20de 100644 --- a/packages/bloom/lib/commands/bloom/INSERT.ts +++ b/packages/bloom/lib/commands/bloom/INSERT.ts @@ -1,45 +1,47 @@ -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; - -export const FIRST_KEY_INDEX = 1; - -interface InsertOptions { - CAPACITY?: number; - ERROR?: number; - EXPANSION?: number; - NOCREATE?: true; - NONSCALING?: true; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; + +export interface BfInsertOptions { + CAPACITY?: number; + ERROR?: number; + EXPANSION?: number; + NOCREATE?: boolean; + NONSCALING?: boolean; } -export function transformArguments( - key: string, - items: RedisCommandArgument | Array, - options?: InsertOptions -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + items: RedisVariadicArgument, + options?: BfInsertOptions + ) { const args = ['BF.INSERT', key]; - if (options?.CAPACITY) { - args.push('CAPACITY', options.CAPACITY.toString()); + if (options?.CAPACITY !== undefined) { + args.push('CAPACITY', options.CAPACITY.toString()); } - if (options?.ERROR) { - args.push('ERROR', options.ERROR.toString()); + if (options?.ERROR !== undefined) { + args.push('ERROR', options.ERROR.toString()); } - if (options?.EXPANSION) { - args.push('EXPANSION', options.EXPANSION.toString()); + if (options?.EXPANSION !== undefined) { + args.push('EXPANSION', options.EXPANSION.toString()); } if (options?.NOCREATE) { - args.push('NOCREATE'); + args.push('NOCREATE'); } if (options?.NONSCALING) { - args.push('NONSCALING'); + args.push('NONSCALING'); } args.push('ITEMS'); - return pushVerdictArguments(args, items); -} - -export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; + return pushVariadicArguments(args, items); + }, + transformReply: transformBooleanArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts b/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts index 19634cb4a78..f958863c0dc 100644 --- a/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts +++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts @@ -1,28 +1,35 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './LOADCHUNK'; +import LOADCHUNK from './LOADCHUNK'; +import { RESP_TYPES } from '@redis/client'; -describe('BF LOADCHUNK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, ''), - ['BF.LOADCHUNK', 'key', '0', ''] - ); - }); +describe('BF.LOADCHUNK', () => { + it('transformArguments', () => { + assert.deepEqual( + LOADCHUNK.transformArguments('key', 0, ''), + ['BF.LOADCHUNK', 'key', '0', ''] + ); + }); - testUtils.testWithClient('client.bf.loadChunk', async client => { - const [, { iterator, chunk }] = await Promise.all([ - client.bf.reserve('source', 0.01, 100), - client.bf.scanDump( - client.commandOptions({ returnBuffers: true }), - 'source', - 0 - ) - ]); + testUtils.testWithClient('client.bf.loadChunk', async client => { + const [, { iterator, chunk }] = await Promise.all([ + client.bf.reserve('source', 0.01, 100), + client.bf.scanDump('source', 0) + ]); - assert.equal( - await client.bf.loadChunk('destination', iterator, chunk), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal( + await client.bf.loadChunk('destination', iterator, chunk), + 'OK' + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + ...GLOBAL.SERVERS.OPEN.clientOptions, + commandOptions: { + typeMapping: { + [RESP_TYPES.BLOB_STRING]: Buffer + } + } + } + }); }); diff --git a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts index 491f572a49e..feade2fac4c 100644 --- a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts @@ -1,13 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: string, - iteretor: number, - chunk: RedisCommandArgument -): RedisCommandArguments { - return ['BF.LOADCHUNK', key, iteretor.toString(), chunk]; -} - -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, iterator: number, chunk: RedisArgument) { + return ['BF.LOADCHUNK', key, iterator.toString(), chunk]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/MADD.spec.ts b/packages/bloom/lib/commands/bloom/MADD.spec.ts index 784f99926ff..5241a09485a 100644 --- a/packages/bloom/lib/commands/bloom/MADD.spec.ts +++ b/packages/bloom/lib/commands/bloom/MADD.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './MADD'; +import MADD from './MADD'; -describe('BF MADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['BF.MADD', 'key', '1', '2'] - ); - }); +describe('BF.MADD', () => { + it('transformArguments', () => { + assert.deepEqual( + MADD.transformArguments('key', ['1', '2']), + ['BF.MADD', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.ts.mAdd', async client => { - assert.deepEqual( - await client.bf.mAdd('key', ['1', '2']), - [true, true] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ts.mAdd', async client => { + assert.deepEqual( + await client.bf.mAdd('key', ['1', '2']), + [true, true] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/bloom/MADD.ts b/packages/bloom/lib/commands/bloom/MADD.ts index 056c4a1c1c2..afb122476fd 100644 --- a/packages/bloom/lib/commands/bloom/MADD.ts +++ b/packages/bloom/lib/commands/bloom/MADD.ts @@ -1,7 +1,12 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export function transformArguments(key: string, items: Array): Array { - return ['BF.MADD', key, ...items]; -} - -export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, items: RedisVariadicArgument) { + return pushVariadicArguments(['BF.MADD', key], items); + }, + transformReply: transformBooleanArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts b/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts index 027e51d2c43..0f313ba636f 100644 --- a/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts +++ b/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './MEXISTS'; +import MEXISTS from './MEXISTS'; -describe('BF MEXISTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['BF.MEXISTS', 'key', '1', '2'] - ); - }); +describe('BF.MEXISTS', () => { + it('transformArguments', () => { + assert.deepEqual( + MEXISTS.transformArguments('key', ['1', '2']), + ['BF.MEXISTS', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.bf.mExists', async client => { - assert.deepEqual( - await client.bf.mExists('key', ['1', '2']), - [false, false] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.bf.mExists', async client => { + assert.deepEqual( + await client.bf.mExists('key', ['1', '2']), + [false, false] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/bloom/MEXISTS.ts b/packages/bloom/lib/commands/bloom/MEXISTS.ts index fb79410155d..a23b713b50c 100644 --- a/packages/bloom/lib/commands/bloom/MEXISTS.ts +++ b/packages/bloom/lib/commands/bloom/MEXISTS.ts @@ -1,9 +1,12 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string, items: Array): Array { - return ['BF.MEXISTS', key, ...items]; -} - -export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, items: RedisVariadicArgument) { + return pushVariadicArguments(['BF.MEXISTS', key], items); + }, + transformReply: transformBooleanArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/RESERVE.spec.ts b/packages/bloom/lib/commands/bloom/RESERVE.spec.ts index bc872f9c3f7..caf40d4a48f 100644 --- a/packages/bloom/lib/commands/bloom/RESERVE.spec.ts +++ b/packages/bloom/lib/commands/bloom/RESERVE.spec.ts @@ -1,49 +1,49 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './RESERVE'; +import RESERVE from './RESERVE'; -describe('BF RESERVE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 0.01, 100), - ['BF.RESERVE', 'key', '0.01', '100'] - ); - }); +describe('BF.RESERVE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + RESERVE.transformArguments('key', 0.01, 100), + ['BF.RESERVE', 'key', '0.01', '100'] + ); + }); - it('with EXPANSION', () => { - assert.deepEqual( - transformArguments('key', 0.01, 100, { - EXPANSION: 1 - }), - ['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1'] - ); - }); + it('with EXPANSION', () => { + assert.deepEqual( + RESERVE.transformArguments('key', 0.01, 100, { + EXPANSION: 1 + }), + ['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1'] + ); + }); - it('with NONSCALING', () => { - assert.deepEqual( - transformArguments('key', 0.01, 100, { - NONSCALING: true - }), - ['BF.RESERVE', 'key', '0.01', '100', 'NONSCALING'] - ); - }); + it('with NONSCALING', () => { + assert.deepEqual( + RESERVE.transformArguments('key', 0.01, 100, { + NONSCALING: true + }), + ['BF.RESERVE', 'key', '0.01', '100', 'NONSCALING'] + ); + }); - it('with EXPANSION and NONSCALING', () => { - assert.deepEqual( - transformArguments('key', 0.01, 100, { - EXPANSION: 1, - NONSCALING: true - }), - ['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1', 'NONSCALING'] - ); - }); + it('with EXPANSION and NONSCALING', () => { + assert.deepEqual( + RESERVE.transformArguments('key', 0.01, 100, { + EXPANSION: 1, + NONSCALING: true + }), + ['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1', 'NONSCALING'] + ); }); + }); - testUtils.testWithClient('client.bf.reserve', async client => { - assert.equal( - await client.bf.reserve('bloom', 0.01, 100), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.bf.reserve', async client => { + assert.equal( + await client.bf.reserve('bloom', 0.01, 100), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/bloom/RESERVE.ts b/packages/bloom/lib/commands/bloom/RESERVE.ts index 18d7002f158..6bccb1d1d13 100644 --- a/packages/bloom/lib/commands/bloom/RESERVE.ts +++ b/packages/bloom/lib/commands/bloom/RESERVE.ts @@ -1,16 +1,19 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -interface ReserveOptions { - EXPANSION?: number; - NONSCALING?: true; +export interface BfReserveOptions { + EXPANSION?: number; + NONSCALING?: boolean; } -export function transformArguments( - key: string, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, errorRate: number, capacity: number, - options?: ReserveOptions -): Array { + options?: BfReserveOptions + ) { const args = ['BF.RESERVE', key, errorRate.toString(), capacity.toString()]; if (options?.EXPANSION) { @@ -22,6 +25,6 @@ export function transformArguments( } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts b/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts index 50119590482..a7de98eabe7 100644 --- a/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts +++ b/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts @@ -1,22 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './SCANDUMP'; +import SCANDUMP from './SCANDUMP'; -describe('BF SCANDUMP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0), - ['BF.SCANDUMP', 'key', '0'] - ); - }); +describe('BF.SCANDUMP', () => { + it('transformArguments', () => { + assert.deepEqual( + SCANDUMP.transformArguments('key', 0), + ['BF.SCANDUMP', 'key', '0'] + ); + }); - testUtils.testWithClient('client.bf.scanDump', async client => { - const [, dump] = await Promise.all([ - client.bf.reserve('key', 0.01, 100), - client.bf.scanDump('key', 0) - ]); - assert.equal(typeof dump, 'object'); - assert.equal(typeof dump.iterator, 'number'); - assert.equal(typeof dump.chunk, 'string'); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.bf.scanDump', async client => { + const [, dump] = await Promise.all([ + client.bf.reserve('key', 0.01, 100), + client.bf.scanDump('key', 0) + ]); + assert.equal(typeof dump, 'object'); + assert.equal(typeof dump.iterator, 'number'); + assert.equal(typeof dump.chunk, 'string'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/bloom/SCANDUMP.ts b/packages/bloom/lib/commands/bloom/SCANDUMP.ts index 04b3edc2a1f..588957b1743 100644 --- a/packages/bloom/lib/commands/bloom/SCANDUMP.ts +++ b/packages/bloom/lib/commands/bloom/SCANDUMP.ts @@ -1,24 +1,15 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string, iterator: number): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, iterator: number) { return ['BF.SCANDUMP', key, iterator.toString()]; -} - -type ScanDumpRawReply = [ - iterator: number, - chunk: string -]; - -interface ScanDumpReply { - iterator: number; - chunk: string; -} - -export function transformReply([iterator, chunk]: ScanDumpRawReply): ScanDumpReply { + }, + transformReply(reply: UnwrapReply>) { return { - iterator, - chunk + iterator: reply[0], + chunk: reply[1] }; -} + } +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/index.ts b/packages/bloom/lib/commands/bloom/index.ts index f18b8f71095..a93f79c9c56 100644 --- a/packages/bloom/lib/commands/bloom/index.ts +++ b/packages/bloom/lib/commands/bloom/index.ts @@ -1,33 +1,64 @@ -import * as ADD from './ADD'; -import * as CARD from './CARD'; -import * as EXISTS from './EXISTS'; -import * as INFO from './INFO'; -import * as INSERT from './INSERT'; -import * as LOADCHUNK from './LOADCHUNK'; -import * as MADD from './MADD'; -import * as MEXISTS from './MEXISTS'; -import * as RESERVE from './RESERVE'; -import * as SCANDUMP from './SCANDUMP'; +import type { RedisCommands, TypeMapping } from '@redis/client/dist/lib/RESP/types'; + +import ADD from './ADD'; +import CARD from './CARD'; +import EXISTS from './EXISTS'; +import INFO from './INFO'; +import INSERT from './INSERT'; +import LOADCHUNK from './LOADCHUNK'; +import MADD from './MADD'; +import MEXISTS from './MEXISTS'; +import RESERVE from './RESERVE'; +import SCANDUMP from './SCANDUMP'; +import { RESP_TYPES } from '@redis/client'; export default { - ADD, - add: ADD, - CARD, - card: CARD, - EXISTS, - exists: EXISTS, - INFO, - info: INFO, - INSERT, - insert: INSERT, - LOADCHUNK, - loadChunk: LOADCHUNK, - MADD, - mAdd: MADD, - MEXISTS, - mExists: MEXISTS, - RESERVE, - reserve: RESERVE, - SCANDUMP, - scanDump: SCANDUMP -}; + ADD, + add: ADD, + CARD, + card: CARD, + EXISTS, + exists: EXISTS, + INFO, + info: INFO, + INSERT, + insert: INSERT, + LOADCHUNK, + loadChunk: LOADCHUNK, + MADD, + mAdd: MADD, + MEXISTS, + mExists: MEXISTS, + RESERVE, + reserve: RESERVE, + SCANDUMP, + scanDump: SCANDUMP +} as const satisfies RedisCommands; + +export function transformInfoV2Reply(reply: Array, typeMapping?: TypeMapping): T { + const mapType = typeMapping ? typeMapping[RESP_TYPES.MAP] : undefined; + + switch (mapType) { + case Array: { + return reply as unknown as T; + } + case Map: { + const ret = new Map(); + + for (let i = 0; i < reply.length; i += 2) { + ret.set(reply[i].toString(), reply[i + 1]); + } + + return ret as unknown as T; + } + default: { + const ret = Object.create(null); + + for (let i = 0; i < reply.length; i += 2) { + ret[reply[i].toString()] = reply[i + 1]; + } + + return ret as unknown as T; + } + } +} \ No newline at end of file diff --git a/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts index 95bb28e88b5..1d2921cab75 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts @@ -1,41 +1,42 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INCRBY'; +import INCRBY from './INCRBY'; -describe('CMS INCRBY', () => { - describe('transformArguments', () => { - it('single item', () => { - assert.deepEqual( - transformArguments('key', { - item: 'item', - incrementBy: 1 - }), - ['CMS.INCRBY', 'key', 'item', '1'] - ); - }); +describe('CMS.INCRBY', () => { + describe('transformArguments', () => { + it('single item', () => { + assert.deepEqual( + INCRBY.transformArguments('key', { + item: 'item', + incrementBy: 1 + }), + ['CMS.INCRBY', 'key', 'item', '1'] + ); + }); - it('multiple items', () => { - assert.deepEqual( - transformArguments('key', [{ - item: 'a', - incrementBy: 1 - }, { - item: 'b', - incrementBy: 2 - }]), - ['CMS.INCRBY', 'key', 'a', '1', 'b', '2'] - ); - }); + it('multiple items', () => { + assert.deepEqual( + INCRBY.transformArguments('key', [{ + item: 'a', + incrementBy: 1 + }, { + item: 'b', + incrementBy: 2 + }]), + ['CMS.INCRBY', 'key', 'a', '1', 'b', '2'] + ); }); + }); + + testUtils.testWithClient('client.cms.incrBy', async client => { + const [, reply] = await Promise.all([ + client.cms.initByDim('key', 1000, 5), + client.cms.incrBy('key', { + item: 'item', + incrementBy: 1 + }) + ]); - testUtils.testWithClient('client.cms.incrBy', async client => { - await client.cms.initByDim('key', 1000, 5); - assert.deepEqual( - await client.cms.incrBy('key', { - item: 'item', - incrementBy: 1 - }), - [1] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [1]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts index e27fb397cdf..1dfbabbaa49 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts @@ -1,29 +1,32 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -interface IncrByItem { - item: string; - incrementBy: number; +export interface BfIncrByItem { + item: RedisArgument; + incrementBy: number; } -export function transformArguments( - key: string, - items: IncrByItem | Array -): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + items: BfIncrByItem | Array + ) { const args = ['CMS.INCRBY', key]; if (Array.isArray(items)) { - for (const item of items) { - pushIncrByItem(args, item); - } + for (const item of items) { + pushIncrByItem(args, item); + } } else { - pushIncrByItem(args, items); + pushIncrByItem(args, items); } return args; -} + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; -function pushIncrByItem(args: Array, { item, incrementBy }: IncrByItem): void { - args.push(item, incrementBy.toString()); +function pushIncrByItem(args: Array, { item, incrementBy }: BfIncrByItem): void { + args.push(item, incrementBy.toString()); } - -export declare function transformReply(): Array; diff --git a/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts index 0db8a48447e..e650d78d2ed 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts @@ -1,25 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INFO'; +import INFO from './INFO'; -describe('CMS INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['CMS.INFO', 'key'] - ); - }); +describe('CMS.INFO', () => { + it('transformArguments', () => { + assert.deepEqual( + INFO.transformArguments('key'), + ['CMS.INFO', 'key'] + ); + }); - testUtils.testWithClient('client.cms.info', async client => { - await client.cms.initByDim('key', 1000, 5); + testUtils.testWithClient('client.cms.info', async client => { + const width = 1000, + depth = 5, + [, reply] = await Promise.all([ + client.cms.initByDim('key', width, depth), + client.cms.info('key') + ]); - assert.deepEqual( - await client.cms.info('key'), - { - width: 1000, - depth: 5, - count: 0 - } - ); - }, GLOBAL.SERVERS.OPEN); + const expected = Object.create(null); + expected['width'] = width; + expected['depth'] = depth; + expected['count'] = 0; + + assert.deepEqual(reply, expected); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/INFO.ts b/packages/bloom/lib/commands/count-min-sketch/INFO.ts index 6dbfffcb0e0..e4aae5bf47b 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INFO.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INFO.ts @@ -1,30 +1,28 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, TuplesToMapReply, NumberReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { transformInfoV2Reply } from '../bloom'; -export const IS_READ_ONLY = true; +export type CmsInfoReplyMap = TuplesToMapReply<[ + [SimpleStringReply<'width'>, NumberReply], + [SimpleStringReply<'depth'>, NumberReply], + [SimpleStringReply<'count'>, NumberReply] +]>; -export function transformArguments(key: string): Array { - return ['CMS.INFO', key]; -} - -export type InfoRawReply = [ - _: string, - width: number, - _: string, - depth: number, - _: string, - count: number -]; - -export interface InfoReply { - width: number; - depth: number; - count: number; -} - -export function transformReply(reply: InfoRawReply): InfoReply { - return { - width: reply[1], - depth: reply[3], - count: reply[5] - }; +export interface CmsInfoReply { + width: NumberReply; + depth: NumberReply; + count: NumberReply; } + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { + return ['CMS.INFO', key]; + }, + transformReply: { + 2: (reply: UnwrapReply>, _, typeMapping?: TypeMapping): CmsInfoReply => { + return transformInfoV2Reply(reply, typeMapping); + }, + 3: undefined as unknown as () => CmsInfoReply + } +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts index 2a9014b765a..a3d27c17df3 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INITBYDIM'; +import INITBYDIM from './INITBYDIM'; -describe('CMS INITBYDIM', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1000, 5), - ['CMS.INITBYDIM', 'key', '1000', '5'] - ); - }); +describe('CMS.INITBYDIM', () => { + it('transformArguments', () => { + assert.deepEqual( + INITBYDIM.transformArguments('key', 1000, 5), + ['CMS.INITBYDIM', 'key', '1000', '5'] + ); + }); - testUtils.testWithClient('client.cms.initByDim', async client => { - assert.equal( - await client.cms.initByDim('key', 1000, 5), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.cms.initByDim', async client => { + assert.equal( + await client.cms.initByDim('key', 1000, 5), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts index 4ec6cedd9ea..60790d421e4 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts @@ -1,7 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, width: number, depth: number): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, width: number, depth: number) { return ['CMS.INITBYDIM', key, width.toString(), depth.toString()]; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts index 004d3df14ef..8df62020e89 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INITBYPROB'; +import INITBYPROB from './INITBYPROB'; -describe('CMS INITBYPROB', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0.001, 0.01), - ['CMS.INITBYPROB', 'key', '0.001', '0.01'] - ); - }); +describe('CMS.INITBYPROB', () => { + it('transformArguments', () => { + assert.deepEqual( + INITBYPROB.transformArguments('key', 0.001, 0.01), + ['CMS.INITBYPROB', 'key', '0.001', '0.01'] + ); + }); - testUtils.testWithClient('client.cms.initByProb', async client => { - assert.equal( - await client.cms.initByProb('key', 0.001, 0.01), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.cms.initByProb', async client => { + assert.equal( + await client.cms.initByProb('key', 0.001, 0.01), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts index 7f0256515fb..7b21755f17d 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts @@ -1,7 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, error: number, probability: number): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, error: number, probability: number) { return ['CMS.INITBYPROB', key, error.toString(), probability.toString()]; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts b/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts index cf234e5734f..eef4bd403ae 100644 --- a/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts @@ -1,36 +1,34 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './MERGE'; +import MERGE from './MERGE'; -describe('CMS MERGE', () => { - describe('transformArguments', () => { - it('without WEIGHTS', () => { - assert.deepEqual( - transformArguments('dest', ['src']), - ['CMS.MERGE', 'dest', '1', 'src'] - ); - }); +describe('CMS.MERGE', () => { + describe('transformArguments', () => { + it('without WEIGHTS', () => { + assert.deepEqual( + MERGE.transformArguments('destination', ['source']), + ['CMS.MERGE', 'destination', '1', 'source'] + ); + }); - it('with WEIGHTS', () => { - assert.deepEqual( - transformArguments('dest', [{ - name: 'src', - weight: 1 - }]), - ['CMS.MERGE', 'dest', '1', 'src', 'WEIGHTS', '1'] - ); - }); + it('with WEIGHTS', () => { + assert.deepEqual( + MERGE.transformArguments('destination', [{ + name: 'source', + weight: 1 + }]), + ['CMS.MERGE', 'destination', '1', 'source', 'WEIGHTS', '1'] + ); }); + }); - testUtils.testWithClient('client.cms.merge', async client => { - await Promise.all([ - client.cms.initByDim('src', 1000, 5), - client.cms.initByDim('dest', 1000, 5), - ]); + testUtils.testWithClient('client.cms.merge', async client => { + const [, , reply] = await Promise.all([ + client.cms.initByDim('source', 1000, 5), + client.cms.initByDim('destination', 1000, 5), + client.cms.merge('destination', ['source']) + ]); - assert.equal( - await client.cms.merge('dest', ['src']), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts index 6cca4e797cd..2e63065d1cc 100644 --- a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts +++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts @@ -1,37 +1,37 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -interface Sketch { - name: string; - weight: number; +interface BfMergeSketch { + name: RedisArgument; + weight: number; } -type Sketches = Array | Array; +export type BfMergeSketches = Array | Array; -export function transformArguments(dest: string, src: Sketches): Array { - const args = [ - 'CMS.MERGE', - dest, - src.length.toString() - ]; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + destination: RedisArgument, + source: BfMergeSketches + ) { + let args = ['CMS.MERGE', destination, source.length.toString()]; - if (isStringSketches(src)) { - args.push(...src); + if (isPlainSketches(source)) { + args = args.concat(source); } else { - for (const sketch of src) { - args.push(sketch.name); - } - - args.push('WEIGHTS'); - for (const sketch of src) { - args.push(sketch.weight.toString()); - } + const { length } = args; + args[length + source.length] = 'WEIGHTS'; + for (let i = 0; i < source.length; i++) { + args[length + i] = source[i].name; + args[length + source.length + i + 1] = source[i].weight.toString(); + } } return args; -} + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; -function isStringSketches(src: Sketches): src is Array { - return typeof src[0] === 'string'; +function isPlainSketches(src: BfMergeSketches): src is Array { + return typeof src[0] === 'string' || src[0] instanceof Buffer; } - -export declare function transformReply(): 'OK'; diff --git a/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts b/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts index d391ab838be..cc9c913b563 100644 --- a/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts @@ -1,22 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './QUERY'; +import QUERY from './QUERY'; -describe('CMS QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CMS.QUERY', 'key', 'item'] - ); - }); +describe('CMS.QUERY', () => { + it('transformArguments', () => { + assert.deepEqual( + QUERY.transformArguments('key', 'item'), + ['CMS.QUERY', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.cms.query', async client => { - await client.cms.initByDim('key', 1000, 5); + testUtils.testWithClient('client.cms.query', async client => { + const [, reply] = await Promise.all([ + client.cms.initByDim('key', 1000, 5), + client.cms.query('key', 'item') + ]); - assert.deepEqual( - await client.cms.query('key', 'item'), - [0] - ); - - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [0]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts index 13a71c9b1de..5d2905300b1 100644 --- a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts @@ -1,15 +1,11 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: string, - items: string | Array -): RedisCommandArguments { - return pushVerdictArguments(['CMS.QUERY', key], items); -} - -export declare function transformReply(): Array; +import { ArrayReply, NumberReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, items: RedisVariadicArgument) { + return pushVariadicArguments(['CMS.QUERY', key], items); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/count-min-sketch/index.ts b/packages/bloom/lib/commands/count-min-sketch/index.ts index 1d61734a8d0..4f0f395ca3d 100644 --- a/packages/bloom/lib/commands/count-min-sketch/index.ts +++ b/packages/bloom/lib/commands/count-min-sketch/index.ts @@ -1,21 +1,22 @@ -import * as INCRBY from './INCRBY'; -import * as INFO from './INFO'; -import * as INITBYDIM from './INITBYDIM'; -import * as INITBYPROB from './INITBYPROB'; -import * as MERGE from './MERGE'; -import * as QUERY from './QUERY'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import INCRBY from './INCRBY'; +import INFO from './INFO'; +import INITBYDIM from './INITBYDIM'; +import INITBYPROB from './INITBYPROB'; +import MERGE from './MERGE'; +import QUERY from './QUERY'; export default { - INCRBY, - incrBy: INCRBY, - INFO, - info: INFO, - INITBYDIM, - initByDim: INITBYDIM, - INITBYPROB, - initByProb: INITBYPROB, - MERGE, - merge: MERGE, - QUERY, - query: QUERY -}; + INCRBY, + incrBy: INCRBY, + INFO, + info: INFO, + INITBYDIM, + initByDim: INITBYDIM, + INITBYPROB, + initByProb: INITBYPROB, + MERGE, + merge: MERGE, + QUERY, + query: QUERY +} as const satisfies RedisCommands; diff --git a/packages/bloom/lib/commands/cuckoo/ADD.spec.ts b/packages/bloom/lib/commands/cuckoo/ADD.spec.ts index f2c029fad3d..fa610cc6666 100644 --- a/packages/bloom/lib/commands/cuckoo/ADD.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/ADD.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments, transformReply } from './ADD'; +import ADD from './ADD'; -describe('CF ADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.ADD', 'key', 'item'] - ); - }); +describe('CF.ADD', () => { + it('transformArguments', () => { + assert.deepEqual( + ADD.transformArguments('key', 'item'), + ['CF.ADD', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.cf.add', async client => { - assert.equal( - await client.cf.add('key', 'item'), - true - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.cf.add', async client => { + assert.equal( + await client.cf.add('key', 'item'), + true + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/ADD.ts b/packages/bloom/lib/commands/cuckoo/ADD.ts index 8d16c0f2ed0..52e98a801d4 100644 --- a/packages/bloom/lib/commands/cuckoo/ADD.ts +++ b/packages/bloom/lib/commands/cuckoo/ADD.ts @@ -1,7 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export function transformArguments(key: string, item: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, item: RedisArgument) { return ['CF.ADD', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts b/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts index ddd9f922b13..f50ad87dc15 100644 --- a/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts @@ -1,21 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './ADDNX'; +import ADDNX from './ADDNX'; -describe('CF ADDNX', () => { - describe('transformArguments', () => { - it('basic add', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.ADDNX', 'key', 'item'] - ); - }); - }); +describe('CF.ADDNX', () => { + it('transformArguments', () => { + assert.deepEqual( + ADDNX.transformArguments('key', 'item'), + ['CF.ADDNX', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.cf.add', async client => { - assert.equal( - await client.cf.addNX('key', 'item'), - true - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.cf.add', async client => { + assert.equal( + await client.cf.addNX('key', 'item'), + true + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/ADDNX.ts b/packages/bloom/lib/commands/cuckoo/ADDNX.ts index 789003a3a57..c739077ee46 100644 --- a/packages/bloom/lib/commands/cuckoo/ADDNX.ts +++ b/packages/bloom/lib/commands/cuckoo/ADDNX.ts @@ -1,7 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export function transformArguments(key: string, item: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, item: RedisArgument) { return ['CF.ADDNX', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts b/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts index 29f5b415935..ff8d40f064e 100644 --- a/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './COUNT'; +import COUNT from './COUNT'; -describe('CF COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.COUNT', 'key', 'item'] - ); - }); +describe('CF.COUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + COUNT.transformArguments('key', 'item'), + ['CF.COUNT', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.cf.count', async client => { - assert.equal( - await client.cf.count('key', 'item'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.cf.count', async client => { + assert.equal( + await client.cf.count('key', 'item'), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/COUNT.ts b/packages/bloom/lib/commands/cuckoo/COUNT.ts index c9f3e28b38a..2284edff174 100644 --- a/packages/bloom/lib/commands/cuckoo/COUNT.ts +++ b/packages/bloom/lib/commands/cuckoo/COUNT.ts @@ -1,7 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, item: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, item: RedisArgument) { return ['CF.COUNT', key, item]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/DEL.spec.ts b/packages/bloom/lib/commands/cuckoo/DEL.spec.ts index 03da65881c1..e02b5636e12 100644 --- a/packages/bloom/lib/commands/cuckoo/DEL.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/DEL.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './DEL'; +import DEL from './DEL'; -describe('CF DEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.DEL', 'key', 'item'] - ); - }); +describe('CF.DEL', () => { + it('transformArguments', () => { + assert.deepEqual( + DEL.transformArguments('key', 'item'), + ['CF.DEL', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.cf.del', async client => { - await client.cf.reserve('key', 4); + testUtils.testWithClient('client.cf.del', async client => { + const [, reply] = await Promise.all([ + client.cf.reserve('key', 4), + client.cf.del('key', 'item') + ]); - assert.equal( - await client.cf.del('key', 'item'), - false - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, false); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/DEL.ts b/packages/bloom/lib/commands/cuckoo/DEL.ts index 1c395a515a8..0af8ebc851b 100644 --- a/packages/bloom/lib/commands/cuckoo/DEL.ts +++ b/packages/bloom/lib/commands/cuckoo/DEL.ts @@ -1,7 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export function transformArguments(key: string, item: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, item: RedisArgument) { return ['CF.DEL', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts b/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts index e281bde6d8a..899c11e8394 100644 --- a/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './EXISTS'; +import EXISTS from './EXISTS'; -describe('CF EXISTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['CF.EXISTS', 'key', 'item'] - ); - }); +describe('CF.EXISTS', () => { + it('transformArguments', () => { + assert.deepEqual( + EXISTS.transformArguments('key', 'item'), + ['CF.EXISTS', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.cf.exists', async client => { - assert.equal( - await client.cf.exists('key', 'item'), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.cf.exists', async client => { + assert.equal( + await client.cf.exists('key', 'item'), + false + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/EXISTS.ts b/packages/bloom/lib/commands/cuckoo/EXISTS.ts index b50a1e25a87..8fd74ca47ca 100644 --- a/packages/bloom/lib/commands/cuckoo/EXISTS.ts +++ b/packages/bloom/lib/commands/cuckoo/EXISTS.ts @@ -1,9 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string, item: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, item: RedisArgument) { return ['CF.EXISTS', key, item]; -} - -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; + }, + transformReply: transformBooleanReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/INFO.spec.ts b/packages/bloom/lib/commands/cuckoo/INFO.spec.ts index c2ac5de6fe0..222177c4650 100644 --- a/packages/bloom/lib/commands/cuckoo/INFO.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/INFO.spec.ts @@ -1,27 +1,29 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INFO'; +import INFO from './INFO'; -describe('CF INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('cuckoo'), - ['CF.INFO', 'cuckoo'] - ); - }); +describe('CF.INFO', () => { + it('transformArguments', () => { + assert.deepEqual( + INFO.transformArguments('cuckoo'), + ['CF.INFO', 'cuckoo'] + ); + }); - testUtils.testWithClient('client.cf.info', async client => { - await client.cf.reserve('key', 4); + testUtils.testWithClient('client.cf.info', async client => { + const [, reply] = await Promise.all([ + client.cf.reserve('key', 4), + client.cf.info('key') + ]); - const info = await client.cf.info('key'); - assert.equal(typeof info, 'object'); - assert.equal(typeof info.size, 'number'); - assert.equal(typeof info.numberOfBuckets, 'number'); - assert.equal(typeof info.numberOfFilters, 'number'); - assert.equal(typeof info.numberOfInsertedItems, 'number'); - assert.equal(typeof info.numberOfDeletedItems, 'number'); - assert.equal(typeof info.bucketSize, 'number'); - assert.equal(typeof info.expansionRate, 'number'); - assert.equal(typeof info.maxIteration, 'number'); - }, GLOBAL.SERVERS.OPEN); + assert.equal(typeof reply, 'object'); + assert.equal(typeof reply['Size'], 'number'); + assert.equal(typeof reply['Number of buckets'], 'number'); + assert.equal(typeof reply['Number of filters'], 'number'); + assert.equal(typeof reply['Number of items inserted'], 'number'); + assert.equal(typeof reply['Number of items deleted'], 'number'); + assert.equal(typeof reply['Bucket size'], 'number'); + assert.equal(typeof reply['Expansion rate'], 'number'); + assert.equal(typeof reply['Max iterations'], 'number'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/INFO.ts b/packages/bloom/lib/commands/cuckoo/INFO.ts index 04d6954e37a..70a7d80c6f2 100644 --- a/packages/bloom/lib/commands/cuckoo/INFO.ts +++ b/packages/bloom/lib/commands/cuckoo/INFO.ts @@ -1,50 +1,27 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { transformInfoV2Reply } from '../bloom'; -export const IS_READ_ONLY = true; +export type CfInfoReplyMap = TuplesToMapReply<[ + [SimpleStringReply<'Size'>, NumberReply], + [SimpleStringReply<'Number of buckets'>, NumberReply], + [SimpleStringReply<'Number of filters'>, NumberReply], + [SimpleStringReply<'Number of items inserted'>, NumberReply], + [SimpleStringReply<'Number of items deleted'>, NumberReply], + [SimpleStringReply<'Bucket size'>, NumberReply], + [SimpleStringReply<'Expansion rate'>, NumberReply], + [SimpleStringReply<'Max iterations'>, NumberReply] +]>; -export function transformArguments(key: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['CF.INFO', key]; -} - -export type InfoRawReply = [ - _: string, - size: number, - _: string, - numberOfBuckets: number, - _: string, - numberOfFilters: number, - _: string, - numberOfInsertedItems: number, - _: string, - numberOfDeletedItems: number, - _: string, - bucketSize: number, - _: string, - expansionRate: number, - _: string, - maxIteration: number -]; - -export interface InfoReply { - size: number; - numberOfBuckets: number; - numberOfFilters: number; - numberOfInsertedItems: number; - numberOfDeletedItems: number; - bucketSize: number; - expansionRate: number; - maxIteration: number; -} - -export function transformReply(reply: InfoRawReply): InfoReply { - return { - size: reply[1], - numberOfBuckets: reply[3], - numberOfFilters: reply[5], - numberOfInsertedItems: reply[7], - numberOfDeletedItems: reply[9], - bucketSize: reply[11], - expansionRate: reply[13], - maxIteration: reply[15] - }; -} + }, + transformReply: { + 2: (reply: UnwrapReply>, _, typeMapping?: TypeMapping): CfInfoReplyMap => { + return transformInfoV2Reply(reply, typeMapping); + }, + 3: undefined as unknown as () => CfInfoReplyMap + } +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts b/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts index 9b56b86a6b7..096cf547098 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts @@ -1,22 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INSERT'; +import INSERT from './INSERT'; -describe('CF INSERT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item', { - CAPACITY: 100, - NOCREATE: true - }), - ['CF.INSERT', 'key', 'CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item'] - ); - }); +describe('CF.INSERT', () => { + it('transformArguments', () => { + assert.deepEqual( + INSERT.transformArguments('key', 'item', { + CAPACITY: 100, + NOCREATE: true + }), + ['CF.INSERT', 'key', 'CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item'] + ); + }); - testUtils.testWithClient('client.cf.insert', async client => { - assert.deepEqual( - await client.cf.insert('key', 'item'), - [true] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.cf.insert', async client => { + assert.deepEqual( + await client.cf.insert('key', 'item'), + [true] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/INSERT.ts b/packages/bloom/lib/commands/cuckoo/INSERT.ts index bcfd4f13a88..d6df64eea1a 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERT.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERT.ts @@ -1,18 +1,34 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { InsertOptions, pushInsertOptions } from "."; +import { Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments, transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export const FIRST_KEY_INDEX = 1; +export interface CfInsertOptions { + CAPACITY?: number; + NOCREATE?: boolean; +} + +export function transofrmCfInsertArguments( + command: RedisArgument, + key: RedisArgument, + items: RedisVariadicArgument, + options?: CfInsertOptions +) { + const args = [command, key]; + + if (options?.CAPACITY !== undefined) { + args.push('CAPACITY', options.CAPACITY.toString()); + } + + if (options?.NOCREATE) { + args.push('NOCREATE'); + } -export function transformArguments( - key: string, - items: string | Array, - options?: InsertOptions -): RedisCommandArguments { - return pushInsertOptions( - ['CF.INSERT', key], - items, - options - ); + args.push('ITEMS'); + return pushVariadicArguments(args, items); } -export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments: transofrmCfInsertArguments.bind(undefined, 'CF.INSERT'), + transformReply: transformBooleanArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts b/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts index 7b1d974e5a6..0f874278220 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts @@ -1,22 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INSERTNX'; +import INSERTNX from './INSERTNX'; -describe('CF INSERTNX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item', { - CAPACITY: 100, - NOCREATE: true - }), - ['CF.INSERTNX', 'key', 'CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item'] - ); - }); +describe('CF.INSERTNX', () => { + it('transformArguments', () => { + assert.deepEqual( + INSERTNX.transformArguments('key', 'item', { + CAPACITY: 100, + NOCREATE: true + }), + ['CF.INSERTNX', 'key', 'CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item'] + ); + }); - testUtils.testWithClient('client.cf.insertnx', async client => { - assert.deepEqual( - await client.cf.insertNX('key', 'item'), - [true] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.cf.insertnx', async client => { + assert.deepEqual( + await client.cf.insertNX('key', 'item'), + [true] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts index 17009e35a42..5cd56e794f9 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts @@ -1,18 +1,9 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { InsertOptions, pushInsertOptions } from "."; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import INSERT, { transofrmCfInsertArguments } from './INSERT'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: string, - items: string | Array, - options?: InsertOptions -): RedisCommandArguments { - return pushInsertOptions( - ['CF.INSERTNX', key], - items, - options - ); -} - -export { transformBooleanArrayReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + FIRST_KEY_INDEX: INSERT.FIRST_KEY_INDEX, + IS_READ_ONLY: INSERT.IS_READ_ONLY, + transformArguments: transofrmCfInsertArguments.bind(undefined, 'CF.INSERTNX'), + transformReply: INSERT.transformReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts index ca3d6f2f8f7..5b880e0dd9d 100644 --- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts @@ -1,31 +1,36 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './LOADCHUNK'; +import LOADCHUNK from './LOADCHUNK'; +import { RESP_TYPES } from '@redis/client'; -describe('CF LOADCHUNK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('item', 0, ''), - ['CF.LOADCHUNK', 'item', '0', ''] - ); - }); +describe('CF.LOADCHUNK', () => { + it('transformArguments', () => { + assert.deepEqual( + LOADCHUNK.transformArguments('item', 0, ''), + ['CF.LOADCHUNK', 'item', '0', ''] + ); + }); - testUtils.testWithClient('client.cf.loadChunk', async client => { - const [,, { iterator, chunk }] = await Promise.all([ - client.cf.reserve('source', 4), - client.cf.add('source', 'item'), - client.cf.scanDump( - client.commandOptions({ returnBuffers: true }), - 'source', - 0 - ) - ]); + testUtils.testWithClient('client.cf.loadChunk', async client => { + const [, , { iterator, chunk }] = await Promise.all([ + client.cf.reserve('source', 4), + client.cf.add('source', 'item'), + client.cf.scanDump('source', 0) + ]); - assert.ok(Buffer.isBuffer(chunk)); - - assert.equal( - await client.cf.loadChunk('destination', iterator, chunk), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal( + await client.cf.loadChunk('destination', iterator, chunk!), + 'OK' + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + ...GLOBAL.SERVERS.OPEN.clientOptions, + commandOptions: { + typeMapping: { + [RESP_TYPES.BLOB_STRING]: Buffer + } + } + } + }); }); diff --git a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts index 6d960c014e2..08cb749b595 100644 --- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts @@ -1,13 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: string, - iterator: number, - chunk: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, iterator: number, chunk: RedisArgument) { return ['CF.LOADCHUNK', key, iterator.toString(), chunk]; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts b/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts index 3145a222c5e..b8f2556bc4f 100644 --- a/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts @@ -1,48 +1,48 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './RESERVE'; +import RESERVE from './RESERVE'; -describe('CF RESERVE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 4), - ['CF.RESERVE', 'key', '4'] - ); - }); +describe('CF.RESERVE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + RESERVE.transformArguments('key', 4), + ['CF.RESERVE', 'key', '4'] + ); + }); - it('with EXPANSION', () => { - assert.deepEqual( - transformArguments('key', 4, { - EXPANSION: 1 - }), - ['CF.RESERVE', 'key', '4', 'EXPANSION', '1'] - ); - }); + it('with EXPANSION', () => { + assert.deepEqual( + RESERVE.transformArguments('key', 4, { + EXPANSION: 1 + }), + ['CF.RESERVE', 'key', '4', 'EXPANSION', '1'] + ); + }); - it('with BUCKETSIZE', () => { - assert.deepEqual( - transformArguments('key', 4, { - BUCKETSIZE: 2 - }), - ['CF.RESERVE', 'key', '4', 'BUCKETSIZE', '2'] - ); - }); + it('with BUCKETSIZE', () => { + assert.deepEqual( + RESERVE.transformArguments('key', 4, { + BUCKETSIZE: 2 + }), + ['CF.RESERVE', 'key', '4', 'BUCKETSIZE', '2'] + ); + }); - it('with MAXITERATIONS', () => { - assert.deepEqual( - transformArguments('key', 4, { - MAXITERATIONS: 1 - }), - ['CF.RESERVE', 'key', '4', 'MAXITERATIONS', '1'] - ); - }); + it('with MAXITERATIONS', () => { + assert.deepEqual( + RESERVE.transformArguments('key', 4, { + MAXITERATIONS: 1 + }), + ['CF.RESERVE', 'key', '4', 'MAXITERATIONS', '1'] + ); }); + }); - testUtils.testWithClient('client.cf.reserve', async client => { - assert.equal( - await client.cf.reserve('key', 4), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.cf.reserve', async client => { + assert.equal( + await client.cf.reserve('key', 4), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/RESERVE.ts b/packages/bloom/lib/commands/cuckoo/RESERVE.ts index 114c1fdf441..bc80daa0087 100644 --- a/packages/bloom/lib/commands/cuckoo/RESERVE.ts +++ b/packages/bloom/lib/commands/cuckoo/RESERVE.ts @@ -1,31 +1,34 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -interface ReserveOptions { - BUCKETSIZE?: number; - MAXITERATIONS?: number; - EXPANSION?: number; +export interface CfReserveOptions { + BUCKETSIZE?: number; + MAXITERATIONS?: number; + EXPANSION?: number; } -export function transformArguments( - key: string, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, capacity: number, - options?: ReserveOptions -): Array { + options?: CfReserveOptions + ) { const args = ['CF.RESERVE', key, capacity.toString()]; - if (options?.BUCKETSIZE) { - args.push('BUCKETSIZE', options.BUCKETSIZE.toString()); + if (options?.BUCKETSIZE !== undefined) { + args.push('BUCKETSIZE', options.BUCKETSIZE.toString()); } - if (options?.MAXITERATIONS) { - args.push('MAXITERATIONS', options.MAXITERATIONS.toString()); + if (options?.MAXITERATIONS !== undefined) { + args.push('MAXITERATIONS', options.MAXITERATIONS.toString()); } - if (options?.EXPANSION) { - args.push('EXPANSION', options.EXPANSION.toString()); + if (options?.EXPANSION !== undefined) { + args.push('EXPANSION', options.EXPANSION.toString()); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts b/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts index ec269c62aa5..e1bac59d323 100644 --- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts @@ -1,23 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './SCANDUMP'; +import SCANDUMP from './SCANDUMP'; -describe('CF SCANDUMP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0), - ['CF.SCANDUMP', 'key', '0'] - ); - }); +describe('CF.SCANDUMP', () => { + it('transformArguments', () => { + assert.deepEqual( + SCANDUMP.transformArguments('key', 0), + ['CF.SCANDUMP', 'key', '0'] + ); + }); + + testUtils.testWithClient('client.cf.scanDump', async client => { + const [, reply] = await Promise.all([ + client.cf.reserve('key', 4), + client.cf.scanDump('key', 0) + ]); - testUtils.testWithClient('client.cf.scanDump', async client => { - await client.cf.reserve('key', 4); - assert.deepEqual( - await client.cf.scanDump('key', 0), - { - iterator: 0, - chunk: null - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, { + iterator: 0, + chunk: null + }); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts index 91476b49a7a..dc076689288 100644 --- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts +++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts @@ -1,22 +1,15 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, iterator: number): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, iterator: number) { return ['CF.SCANDUMP', key, iterator.toString()]; -} - -type ScanDumpRawReply = [ - iterator: number, - chunk: string | null -]; - -interface ScanDumpReply { - iterator: number; - chunk: string | null; -} - -export function transformReply([iterator, chunk]: ScanDumpRawReply): ScanDumpReply { + }, + transformReply(reply: UnwrapReply>) { return { - iterator, - chunk + iterator: reply[0], + chunk: reply[1] }; -} + } +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/index.spec.ts b/packages/bloom/lib/commands/cuckoo/index.spec.ts deleted file mode 100644 index 94f3a0ae281..00000000000 --- a/packages/bloom/lib/commands/cuckoo/index.spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { strict as assert } from 'assert'; -import { pushInsertOptions } from '.'; - -describe('pushInsertOptions', () => { - describe('single item', () => { - it('single item', () => { - assert.deepEqual( - pushInsertOptions([], 'item'), - ['ITEMS', 'item'] - ); - }); - - it('multiple items', () => { - assert.deepEqual( - pushInsertOptions([], ['1', '2']), - ['ITEMS', '1', '2'] - ); - }); - }); - - it('with CAPACITY', () => { - assert.deepEqual( - pushInsertOptions([], 'item', { - CAPACITY: 100 - }), - ['CAPACITY', '100', 'ITEMS', 'item'] - ); - }); - - it('with NOCREATE', () => { - assert.deepEqual( - pushInsertOptions([], 'item', { - NOCREATE: true - }), - ['NOCREATE', 'ITEMS', 'item'] - ); - }); - - it('with CAPACITY and NOCREATE', () => { - assert.deepEqual( - pushInsertOptions([], 'item', { - CAPACITY: 100, - NOCREATE: true - }), - ['CAPACITY', '100', 'NOCREATE', 'ITEMS', 'item'] - ); - }); -}); diff --git a/packages/bloom/lib/commands/cuckoo/index.ts b/packages/bloom/lib/commands/cuckoo/index.ts index 96b4453bc39..62c63fe8d19 100644 --- a/packages/bloom/lib/commands/cuckoo/index.ts +++ b/packages/bloom/lib/commands/cuckoo/index.ts @@ -1,62 +1,37 @@ - -import * as ADD from './ADD'; -import * as ADDNX from './ADDNX'; -import * as COUNT from './COUNT'; -import * as DEL from './DEL'; -import * as EXISTS from './EXISTS'; -import * as INFO from './INFO'; -import * as INSERT from './INSERT'; -import * as INSERTNX from './INSERTNX'; -import * as LOADCHUNK from './LOADCHUNK'; -import * as RESERVE from './RESERVE'; -import * as SCANDUMP from './SCANDUMP'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import ADD from './ADD'; +import ADDNX from './ADDNX'; +import COUNT from './COUNT'; +import DEL from './DEL'; +import EXISTS from './EXISTS'; +import INFO from './INFO'; +import INSERT from './INSERT'; +import INSERTNX from './INSERTNX'; +import LOADCHUNK from './LOADCHUNK'; +import RESERVE from './RESERVE'; +import SCANDUMP from './SCANDUMP'; export default { - ADD, - add: ADD, - ADDNX, - addNX: ADDNX, - COUNT, - count: COUNT, - DEL, - del: DEL, - EXISTS, - exists: EXISTS, - INFO, - info: INFO, - INSERT, - insert: INSERT, - INSERTNX, - insertNX: INSERTNX, - LOADCHUNK, - loadChunk: LOADCHUNK, - RESERVE, - reserve: RESERVE, - SCANDUMP, - scanDump: SCANDUMP -}; - -export interface InsertOptions { - CAPACITY?: number; - NOCREATE?: true; -} - -export function pushInsertOptions( - args: RedisCommandArguments, - items: string | Array, - options?: InsertOptions -): RedisCommandArguments { - if (options?.CAPACITY) { - args.push('CAPACITY'); - args.push(options.CAPACITY.toString()); - } - - if (options?.NOCREATE) { - args.push('NOCREATE'); - } - - args.push('ITEMS'); - return pushVerdictArguments(args, items); -} + ADD, + add: ADD, + ADDNX, + addNX: ADDNX, + COUNT, + count: COUNT, + DEL, + del: DEL, + EXISTS, + exists: EXISTS, + INFO, + info: INFO, + INSERT, + insert: INSERT, + INSERTNX, + insertNX: INSERTNX, + LOADCHUNK, + loadChunk: LOADCHUNK, + RESERVE, + reserve: RESERVE, + SCANDUMP, + scanDump: SCANDUMP +} as const satisfies RedisCommands; diff --git a/packages/bloom/lib/commands/index.ts b/packages/bloom/lib/commands/index.ts index cea55b2a7c0..6f91089460a 100644 --- a/packages/bloom/lib/commands/index.ts +++ b/packages/bloom/lib/commands/index.ts @@ -1,3 +1,4 @@ +import { RedisModules } from '@redis/client'; import bf from './bloom'; import cms from './count-min-sketch'; import cf from './cuckoo'; @@ -5,9 +6,9 @@ import tDigest from './t-digest'; import topK from './top-k'; export default { - bf, - cms, - cf, - tDigest, - topK -}; + bf, + cms, + cf, + tDigest, + topK +} as const satisfies RedisModules; diff --git a/packages/bloom/lib/commands/t-digest/ADD.spec.ts b/packages/bloom/lib/commands/t-digest/ADD.spec.ts index 3e1dbff7f27..31d4957c6ad 100644 --- a/packages/bloom/lib/commands/t-digest/ADD.spec.ts +++ b/packages/bloom/lib/commands/t-digest/ADD.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './ADD'; +import ADD from './ADD'; describe('TDIGEST.ADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.ADD', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ADD.transformArguments('key', [1, 2]), + ['TDIGEST.ADD', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.tDigest.add', async client => { - const [ , reply ] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.add('key', [1]) - ]); + testUtils.testWithClient('client.tDigest.add', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.add('key', [1]) + ]); - assert.equal(reply, 'OK'); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/ADD.ts b/packages/bloom/lib/commands/t-digest/ADD.ts index 941e8531003..e7c6d7c4429 100644 --- a/packages/bloom/lib/commands/t-digest/ADD.ts +++ b/packages/bloom/lib/commands/t-digest/ADD.ts @@ -1,17 +1,16 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - values: Array -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, values: Array) { const args = ['TDIGEST.ADD', key]; - for (const item of values) { - args.push(item.toString()); + + for (const value of values) { + args.push(value.toString()); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts b/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts index 083f09d22af..a6443d77432 100644 --- a/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './BYRANK'; +import BYRANK from './BYRANK'; describe('TDIGEST.BYRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.BYRANK', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + BYRANK.transformArguments('key', [1, 2]), + ['TDIGEST.BYRANK', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.tDigest.byRank', async client => { - const [ , reply ] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.byRank('key', [1]) - ]); + testUtils.testWithClient('client.tDigest.byRank', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.byRank('key', [1]) + ]); - assert.deepEqual(reply, [NaN]); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [NaN]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/BYRANK.ts b/packages/bloom/lib/commands/t-digest/BYRANK.ts index 5684385b4d3..8b48acd1b1b 100644 --- a/packages/bloom/lib/commands/t-digest/BYRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYRANK.ts @@ -1,19 +1,24 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export const FIRST_KEY_INDEX = 1; +export function transformByRankArguments( + command: RedisArgument, + key: RedisArgument, + ranks: Array +) { + const args = [command, key]; -export const IS_READ_ONLY = true; + for (const rank of ranks) { + args.push(rank.toString()); + } -export function transformArguments( - key: RedisCommandArgument, - ranks: Array -): RedisCommandArguments { - const args = ['TDIGEST.BYRANK', key]; - for (const rank of ranks) { - args.push(rank.toString()); - } - - return args; + return args; } -export { transformDoublesReply as transformReply } from '.'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments: transformByRankArguments.bind(undefined, 'TDIGEST.BYRANK'), + transformReply: transformDoubleArrayReply +} as const satisfies Command; + diff --git a/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts b/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts index c094f36e71d..f5bb4e62816 100644 --- a/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './BYREVRANK'; +import BYREVRANK from './BYREVRANK'; describe('TDIGEST.BYREVRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.BYREVRANK', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + BYREVRANK.transformArguments('key', [1, 2]), + ['TDIGEST.BYREVRANK', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.tDigest.byRevRank', async client => { - const [ , reply ] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.byRevRank('key', [1]) - ]); + testUtils.testWithClient('client.tDigest.byRevRank', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.byRevRank('key', [1]) + ]); - assert.deepEqual(reply, [NaN]); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [NaN]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts index 3dcf3a973c4..9f62b42d812 100644 --- a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts @@ -1,19 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - ranks: Array -): RedisCommandArguments { - const args = ['TDIGEST.BYREVRANK', key]; - for (const rank of ranks) { - args.push(rank.toString()); - } - - return args; -} - -export { transformDoublesReply as transformReply } from '.'; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import BYRANK, { transformByRankArguments } from './BYRANK'; + +export default { + FIRST_KEY_INDEX: BYRANK.FIRST_KEY_INDEX, + IS_READ_ONLY: BYRANK.IS_READ_ONLY, + transformArguments: transformByRankArguments.bind(undefined, 'TDIGEST.BYREVRANK'), + transformReply: BYRANK.transformReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/CDF.spec.ts b/packages/bloom/lib/commands/t-digest/CDF.spec.ts index 36d3564f62c..09208deba11 100644 --- a/packages/bloom/lib/commands/t-digest/CDF.spec.ts +++ b/packages/bloom/lib/commands/t-digest/CDF.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './CDF'; +import CDF from './CDF'; describe('TDIGEST.CDF', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.CDF', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CDF.transformArguments('key', [1, 2]), + ['TDIGEST.CDF', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.tDigest.cdf', async client => { - const [ , reply ] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.cdf('key', [1]) - ]); + testUtils.testWithClient('client.tDigest.cdf', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.cdf('key', [1]) + ]); - assert.deepEqual(reply, [NaN]); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [NaN]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/CDF.ts b/packages/bloom/lib/commands/t-digest/CDF.ts index fe7ece59d76..0fbdedb3a47 100644 --- a/packages/bloom/lib/commands/t-digest/CDF.ts +++ b/packages/bloom/lib/commands/t-digest/CDF.ts @@ -1,19 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - values: Array -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, values: Array) { const args = ['TDIGEST.CDF', key]; + for (const item of values) { - args.push(item.toString()); + args.push(item.toString()); } return args; -} - -export { transformDoublesReply as transformReply } from '.'; + }, + transformReply: transformDoubleArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/CREATE.spec.ts b/packages/bloom/lib/commands/t-digest/CREATE.spec.ts index 4d329cc81ae..781b2a7e432 100644 --- a/packages/bloom/lib/commands/t-digest/CREATE.spec.ts +++ b/packages/bloom/lib/commands/t-digest/CREATE.spec.ts @@ -1,30 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './CREATE'; +import CREATE from './CREATE'; describe('TDIGEST.CREATE', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.CREATE', 'key'] - ); - }); + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + CREATE.transformArguments('key'), + ['TDIGEST.CREATE', 'key'] + ); + }); - it('with COMPRESSION', () => { - assert.deepEqual( - transformArguments('key', { - COMPRESSION: 100 - }), - ['TDIGEST.CREATE', 'key', 'COMPRESSION', '100'] - ); - }); + it('with COMPRESSION', () => { + assert.deepEqual( + CREATE.transformArguments('key', { + COMPRESSION: 100 + }), + ['TDIGEST.CREATE', 'key', 'COMPRESSION', '100'] + ); }); + }); - testUtils.testWithClient('client.tDigest.create', async client => { - assert.equal( - await client.tDigest.create('key'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.tDigest.create', async client => { + assert.equal( + await client.tDigest.create('key'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/CREATE.ts b/packages/bloom/lib/commands/t-digest/CREATE.ts index 1935d2973dc..8b832487daa 100644 --- a/packages/bloom/lib/commands/t-digest/CREATE.ts +++ b/packages/bloom/lib/commands/t-digest/CREATE.ts @@ -1,16 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { CompressionOption, pushCompressionArgument } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - options?: CompressionOption -): RedisCommandArguments { - return pushCompressionArgument( - ['TDIGEST.CREATE', key], - options - ); +export interface TDigestCreateOptions { + COMPRESSION?: number; } -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: TDigestCreateOptions) { + const args = ['TDIGEST.CREATE', key]; + + if (options?.COMPRESSION !== undefined) { + args.push('COMPRESSION', options.COMPRESSION.toString()); + } + + return args; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/INFO.spec.ts b/packages/bloom/lib/commands/t-digest/INFO.spec.ts index 992fda6ea05..247f4ab0b61 100644 --- a/packages/bloom/lib/commands/t-digest/INFO.spec.ts +++ b/packages/bloom/lib/commands/t-digest/INFO.spec.ts @@ -1,25 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INFO'; +import INFO from './INFO'; describe('TDIGEST.INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.INFO', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + INFO.transformArguments('key'), + ['TDIGEST.INFO', 'key'] + ); + }); - testUtils.testWithClient('client.tDigest.info', async client => { - await client.tDigest.create('key'); + testUtils.testWithClient('client.tDigest.info', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.info('key') + ]); - const info = await client.tDigest.info('key'); - assert(typeof info.capacity, 'number'); - assert(typeof info.mergedNodes, 'number'); - assert(typeof info.unmergedNodes, 'number'); - assert(typeof info.mergedWeight, 'number'); - assert(typeof info.unmergedWeight, 'number'); - assert(typeof info.totalCompression, 'number'); - assert(typeof info.totalCompression, 'number'); - }, GLOBAL.SERVERS.OPEN); + assert(typeof reply, 'object'); + assert(typeof reply['Compression'], 'number'); + assert(typeof reply['Capacity'], 'number'); + assert(typeof reply['Merged nodes'], 'number'); + assert(typeof reply['Unmerged nodes'], 'number'); + assert(typeof reply['Merged weight'], 'number'); + assert(typeof reply['Unmerged weight'], 'number'); + assert(typeof reply['Observations'], 'number'); + assert(typeof reply['Total compressions'], 'number'); + assert(typeof reply['Memory usage'], 'number'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/INFO.ts b/packages/bloom/lib/commands/t-digest/INFO.ts index 44d2083524f..c7c2357d2b4 100644 --- a/packages/bloom/lib/commands/t-digest/INFO.ts +++ b/packages/bloom/lib/commands/t-digest/INFO.ts @@ -1,51 +1,28 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { transformInfoV2Reply } from '../bloom'; -export const FIRST_KEY_INDEX = 1; +export type TdInfoReplyMap = TuplesToMapReply<[ + [SimpleStringReply<'Compression'>, NumberReply], + [SimpleStringReply<'Capacity'>, NumberReply], + [SimpleStringReply<'Merged nodes'>, NumberReply], + [SimpleStringReply<'Unmerged nodes'>, NumberReply], + [SimpleStringReply<'Merged weight'>, NumberReply], + [SimpleStringReply<'Unmerged weight'>, NumberReply], + [SimpleStringReply<'Observations'>, NumberReply], + [SimpleStringReply<'Total compressions'>, NumberReply], + [SimpleStringReply<'Memory usage'>, NumberReply] +]>; -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return [ - 'TDIGEST.INFO', - key - ]; -} - -type InfoRawReply = [ - 'Compression', - number, - 'Capacity', - number, - 'Merged nodes', - number, - 'Unmerged nodes', - number, - 'Merged weight', - string, - 'Unmerged weight', - string, - 'Total compressions', - number -]; - -interface InfoReply { - comperssion: number; - capacity: number; - mergedNodes: number; - unmergedNodes: number; - mergedWeight: number; - unmergedWeight: number; - totalCompression: number; -} - -export function transformReply(reply: InfoRawReply): InfoReply { - return { - comperssion: reply[1], - capacity: reply[3], - mergedNodes: reply[5], - unmergedNodes: reply[7], - mergedWeight: Number(reply[9]), - unmergedWeight: Number(reply[11]), - totalCompression: reply[13] - }; -} \ No newline at end of file +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { + return ['TDIGEST.INFO', key]; + }, + transformReply: { + 2: (reply: UnwrapReply>, _, typeMapping?: TypeMapping): TdInfoReplyMap => { + return transformInfoV2Reply(reply, typeMapping); + }, + 3: undefined as unknown as () => TdInfoReplyMap + } +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/MAX.spec.ts b/packages/bloom/lib/commands/t-digest/MAX.spec.ts index bf850cbfd83..caa92b0a6a0 100644 --- a/packages/bloom/lib/commands/t-digest/MAX.spec.ts +++ b/packages/bloom/lib/commands/t-digest/MAX.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments, transformReply } from './MAX'; +import MAX from './MAX'; describe('TDIGEST.MAX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.MAX', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MAX.transformArguments('key'), + ['TDIGEST.MAX', 'key'] + ); + }); - testUtils.testWithClient('client.tDigest.max', async client => { - const [ , reply ] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.max('key') - ]); + testUtils.testWithClient('client.tDigest.max', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.max('key') + ]); - assert.deepEqual(reply, NaN); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, NaN); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/MAX.ts b/packages/bloom/lib/commands/t-digest/MAX.ts index 90c42ec6067..ef778f832a6 100644 --- a/packages/bloom/lib/commands/t-digest/MAX.ts +++ b/packages/bloom/lib/commands/t-digest/MAX.ts @@ -1,14 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return [ - 'TDIGEST.MAX', - key - ]; -} - -export { transformDoubleReply as transformReply } from '.'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { + return ['TDIGEST.MAX', key]; + }, + transformReply: transformDoubleReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/MERGE.spec.ts b/packages/bloom/lib/commands/t-digest/MERGE.spec.ts index 1205cdd9216..1ee792e3a40 100644 --- a/packages/bloom/lib/commands/t-digest/MERGE.spec.ts +++ b/packages/bloom/lib/commands/t-digest/MERGE.spec.ts @@ -1,50 +1,50 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments, transformReply } from './MERGE'; +import MERGE from './MERGE'; describe('TDIGEST.MERGE', () => { - describe('transformArguments', () => { - describe('srcKeys', () => { - it('string', () => { - assert.deepEqual( - transformArguments('dest', 'src'), - ['TDIGEST.MERGE', 'dest', '1', 'src'] - ); - }); + describe('transformArguments', () => { + describe('source', () => { + it('string', () => { + assert.deepEqual( + MERGE.transformArguments('destination', 'source'), + ['TDIGEST.MERGE', 'destination', '1', 'source'] + ); + }); - it('Array', () => { - assert.deepEqual( - transformArguments('dest', ['1', '2']), - ['TDIGEST.MERGE', 'dest', '2', '1', '2'] - ); - }); - }); + it('Array', () => { + assert.deepEqual( + MERGE.transformArguments('destination', ['1', '2']), + ['TDIGEST.MERGE', 'destination', '2', '1', '2'] + ); + }); + }); - it('with COMPRESSION', () => { - assert.deepEqual( - transformArguments('dest', 'src', { - COMPRESSION: 100 - }), - ['TDIGEST.MERGE', 'dest', '1', 'src', 'COMPRESSION', '100'] - ); - }); + it('with COMPRESSION', () => { + assert.deepEqual( + MERGE.transformArguments('destination', 'source', { + COMPRESSION: 100 + }), + ['TDIGEST.MERGE', 'destination', '1', 'source', 'COMPRESSION', '100'] + ); + }); - it('with OVERRIDE', () => { - assert.deepEqual( - transformArguments('dest', 'src', { - OVERRIDE: true - }), - ['TDIGEST.MERGE', 'dest', '1', 'src', 'OVERRIDE'] - ); - }); + it('with OVERRIDE', () => { + assert.deepEqual( + MERGE.transformArguments('destination', 'source', { + OVERRIDE: true + }), + ['TDIGEST.MERGE', 'destination', '1', 'source', 'OVERRIDE'] + ); }); + }); - testUtils.testWithClient('client.tDigest.merge', async client => { - const [ , reply ] = await Promise.all([ - client.tDigest.create('src'), - client.tDigest.merge('dest', 'src') - ]); + testUtils.testWithClient('client.tDigest.merge', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('source'), + client.tDigest.merge('destination', 'source') + ]); - assert.equal(reply, 'OK'); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/MERGE.ts b/packages/bloom/lib/commands/t-digest/MERGE.ts index 5119d0b9e18..e9cd99aabf9 100644 --- a/packages/bloom/lib/commands/t-digest/MERGE.ts +++ b/packages/bloom/lib/commands/t-digest/MERGE.ts @@ -1,30 +1,30 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { CompressionOption, pushCompressionArgument } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -interface MergeOptions extends CompressionOption { - OVERRIDE?: boolean; +export interface TDigestMergeOptions { + COMPRESSION?: number; + OVERRIDE?: boolean; } -export function transformArguments( - destKey: RedisCommandArgument, - srcKeys: RedisCommandArgument | Array, - options?: MergeOptions -): RedisCommandArguments { - const args = pushVerdictArgument( - ['TDIGEST.MERGE', destKey], - srcKeys - ); +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments( + destination: RedisArgument, + source: RedisVariadicArgument, + options?: TDigestMergeOptions + ) { + const args = pushVariadicArgument(['TDIGEST.MERGE', destination], source); - pushCompressionArgument(args, options); + if (options?.COMPRESSION !== undefined) { + args.push('COMPRESSION', options.COMPRESSION.toString()); + } if (options?.OVERRIDE) { - args.push('OVERRIDE'); + args.push('OVERRIDE'); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/MIN.spec.ts b/packages/bloom/lib/commands/t-digest/MIN.spec.ts index d48deaca7fb..0d1637cc9b7 100644 --- a/packages/bloom/lib/commands/t-digest/MIN.spec.ts +++ b/packages/bloom/lib/commands/t-digest/MIN.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments, transformReply } from './MIN'; +import MIN from './MIN'; describe('TDIGEST.MIN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.MIN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MIN.transformArguments('key'), + ['TDIGEST.MIN', 'key'] + ); + }); - testUtils.testWithClient('client.tDigest.min', async client => { - const [ , reply ] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.min('key') - ]); + testUtils.testWithClient('client.tDigest.min', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.min('key') + ]); - assert.equal(reply, NaN); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, NaN); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/MIN.ts b/packages/bloom/lib/commands/t-digest/MIN.ts index d8be8722b60..914998a1ec4 100644 --- a/packages/bloom/lib/commands/t-digest/MIN.ts +++ b/packages/bloom/lib/commands/t-digest/MIN.ts @@ -1,14 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return [ - 'TDIGEST.MIN', - key - ]; -} - -export { transformDoubleReply as transformReply } from '.'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { + return ['TDIGEST.MIN', key]; + }, + transformReply: transformDoubleReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts b/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts index 7790debf0de..c427f8c4501 100644 --- a/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts +++ b/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts @@ -1,24 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './QUANTILE'; +import QUANTILE from './QUANTILE'; describe('TDIGEST.QUANTILE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.QUANTILE', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + QUANTILE.transformArguments('key', [1, 2]), + ['TDIGEST.QUANTILE', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.tDigest.quantile', async client => { - const [, reply] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.quantile('key', [1]) - ]); + testUtils.testWithClient('client.tDigest.quantile', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.quantile('key', [1]) + ]); - assert.deepEqual( - reply, - [NaN] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + reply, + [NaN] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/QUANTILE.ts b/packages/bloom/lib/commands/t-digest/QUANTILE.ts index 2289ffc6f55..f7057a37d1c 100644 --- a/packages/bloom/lib/commands/t-digest/QUANTILE.ts +++ b/packages/bloom/lib/commands/t-digest/QUANTILE.ts @@ -1,23 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - quantiles: Array -): RedisCommandArguments { - const args = [ - 'TDIGEST.QUANTILE', - key - ]; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, quantiles: Array) { + const args = ['TDIGEST.QUANTILE', key]; for (const quantile of quantiles) { - args.push(quantile.toString()); + args.push(quantile.toString()); } return args; -} - -export { transformDoublesReply as transformReply } from '.'; + }, + transformReply: transformDoubleArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/RANK.spec.ts b/packages/bloom/lib/commands/t-digest/RANK.spec.ts index 258bedf3491..dcdae48cb06 100644 --- a/packages/bloom/lib/commands/t-digest/RANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/RANK.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './RANK'; +import RANK from './RANK'; describe('TDIGEST.RANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.RANK', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + RANK.transformArguments('key', [1, 2]), + ['TDIGEST.RANK', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.tDigest.rank', async client => { - const [ , reply ] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.rank('key', [1]) - ]); + testUtils.testWithClient('client.tDigest.rank', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.rank('key', [1]) + ]); - assert.deepEqual(reply, [-2]); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [-2]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/RANK.ts b/packages/bloom/lib/commands/t-digest/RANK.ts index 1a6c84bbd4d..8c18ad12778 100644 --- a/packages/bloom/lib/commands/t-digest/RANK.ts +++ b/packages/bloom/lib/commands/t-digest/RANK.ts @@ -1,19 +1,22 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; +export function transformRankArguments( + command: RedisArgument, + key: RedisArgument, + values: Array +) { + const args = [command, key]; -export const IS_READ_ONLY = true; + for (const value of values) { + args.push(value.toString()); + } -export function transformArguments( - key: RedisCommandArgument, - values: Array -): RedisCommandArguments { - const args = ['TDIGEST.RANK', key]; - for (const item of values) { - args.push(item.toString()); - } - - return args; + return args; } -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments: transformRankArguments.bind(undefined, 'TDIGEST.RANK'), + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/RESET.spec.ts b/packages/bloom/lib/commands/t-digest/RESET.spec.ts index 036fbebc8cc..072257113b9 100644 --- a/packages/bloom/lib/commands/t-digest/RESET.spec.ts +++ b/packages/bloom/lib/commands/t-digest/RESET.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './RESET'; +import RESET from './RESET'; describe('TDIGEST.RESET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TDIGEST.RESET', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + RESET.transformArguments('key'), + ['TDIGEST.RESET', 'key'] + ); + }); - testUtils.testWithClient('client.tDigest.reset', async client => { - const [, reply] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.reset('key') - ]); + testUtils.testWithClient('client.tDigest.reset', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.reset('key') + ]); - assert.equal(reply, 'OK'); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/RESET.ts b/packages/bloom/lib/commands/t-digest/RESET.ts index 6c700e6b932..372a1efd488 100644 --- a/packages/bloom/lib/commands/t-digest/RESET.ts +++ b/packages/bloom/lib/commands/t-digest/RESET.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument) { return ['TDIGEST.RESET', key]; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts b/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts index 21d16661dfe..baa1b94afa8 100644 --- a/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './REVRANK'; +import REVRANK from './REVRANK'; describe('TDIGEST.REVRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [1, 2]), - ['TDIGEST.REVRANK', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + REVRANK.transformArguments('key', [1, 2]), + ['TDIGEST.REVRANK', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.tDigest.revRank', async client => { - const [ , reply ] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.revRank('key', [1]) - ]); + testUtils.testWithClient('client.tDigest.revRank', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.revRank('key', [1]) + ]); - assert.deepEqual(reply, [-2]); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [-2]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/REVRANK.ts b/packages/bloom/lib/commands/t-digest/REVRANK.ts index a2465052774..456b2be5a38 100644 --- a/packages/bloom/lib/commands/t-digest/REVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/REVRANK.ts @@ -1,19 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - values: Array -): RedisCommandArguments { - const args = ['TDIGEST.REVRANK', key]; - for (const item of values) { - args.push(item.toString()); - } - - return args; -} - -export declare function transformReply(): Array; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import RANK, { transformRankArguments } from './RANK'; + +export default { + FIRST_KEY_INDEX: RANK.FIRST_KEY_INDEX, + IS_READ_ONLY: RANK.IS_READ_ONLY, + transformArguments: transformRankArguments.bind(undefined, 'TDIGEST.REVRANK'), + transformReply: RANK.transformReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts index dd07f325c8d..c43c0f47553 100644 --- a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts +++ b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments, transformReply } from './TRIMMED_MEAN'; +import TRIMMED_MEAN from './TRIMMED_MEAN'; -describe('TDIGEST.RESET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 1), - ['TDIGEST.TRIMMED_MEAN', 'key', '0', '1'] - ); - }); +describe('TDIGEST.TRIMMED_MEAN', () => { + it('transformArguments', () => { + assert.deepEqual( + TRIMMED_MEAN.transformArguments('key', 0, 1), + ['TDIGEST.TRIMMED_MEAN', 'key', '0', '1'] + ); + }); - testUtils.testWithClient('client.tDigest.trimmedMean', async client => { - const [, reply] = await Promise.all([ - client.tDigest.create('key'), - client.tDigest.trimmedMean('key', 0, 1) - ]); + testUtils.testWithClient('client.tDigest.trimmedMean', async client => { + const [, reply] = await Promise.all([ + client.tDigest.create('key'), + client.tDigest.trimmedMean('key', 0, 1) + ]); - assert.equal(reply, NaN); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, NaN); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts index 6de80ba7c7c..f91dd7d8093 100644 --- a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts +++ b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts @@ -1,20 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, lowCutPercentile: number, highCutPercentile: number -): RedisCommandArguments { + ) { return [ - 'TDIGEST.TRIMMED_MEAN', - key, - lowCutPercentile.toString(), - highCutPercentile.toString() + 'TDIGEST.TRIMMED_MEAN', + key, + lowCutPercentile.toString(), + highCutPercentile.toString() ]; -} - -export { transformDoubleReply as transformReply } from '.'; + }, + transformReply: transformDoubleReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/index.spec.ts b/packages/bloom/lib/commands/t-digest/index.spec.ts deleted file mode 100644 index 5bef6df04b2..00000000000 --- a/packages/bloom/lib/commands/t-digest/index.spec.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { strict as assert } from 'assert'; -import { pushCompressionArgument, transformDoubleReply, transformDoublesReply } from '.'; - -describe('pushCompressionArgument', () => { - it('undefined', () => { - assert.deepEqual( - pushCompressionArgument([]), - [] - ); - }); - - it('100', () => { - assert.deepEqual( - pushCompressionArgument([], { COMPRESSION: 100 }), - ['COMPRESSION', '100'] - ); - }); -}); - -describe('transformDoubleReply', () => { - it('inf', () => { - assert.equal( - transformDoubleReply('inf'), - Infinity - ); - }); - - it('-inf', () => { - assert.equal( - transformDoubleReply('-inf'), - -Infinity - ); - }); - - it('nan', () => { - assert.equal( - transformDoubleReply('nan'), - NaN - ); - }); - - it('0', () => { - assert.equal( - transformDoubleReply('0'), - 0 - ); - }); -}); - -it('transformDoublesReply', () => { - assert.deepEqual( - transformDoublesReply(['inf', '-inf', 'nan', '0']), - [Infinity, -Infinity, NaN, 0] - ); -}); diff --git a/packages/bloom/lib/commands/t-digest/index.ts b/packages/bloom/lib/commands/t-digest/index.ts index da3b37464d2..d180911dbf9 100644 --- a/packages/bloom/lib/commands/t-digest/index.ts +++ b/packages/bloom/lib/commands/t-digest/index.ts @@ -1,81 +1,46 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import * as ADD from './ADD'; -import * as BYRANK from './BYRANK'; -import * as BYREVRANK from './BYREVRANK'; -import * as CDF from './CDF'; -import * as CREATE from './CREATE'; -import * as INFO from './INFO'; -import * as MAX from './MAX'; -import * as MERGE from './MERGE'; -import * as MIN from './MIN'; -import * as QUANTILE from './QUANTILE'; -import * as RANK from './RANK'; -import * as RESET from './RESET'; -import * as REVRANK from './REVRANK'; -import * as TRIMMED_MEAN from './TRIMMED_MEAN'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import ADD from './ADD'; +import BYRANK from './BYRANK'; +import BYREVRANK from './BYREVRANK'; +import CDF from './CDF'; +import CREATE from './CREATE'; +import INFO from './INFO'; +import MAX from './MAX'; +import MERGE from './MERGE'; +import MIN from './MIN'; +import QUANTILE from './QUANTILE'; +import RANK from './RANK'; +import RESET from './RESET'; +import REVRANK from './REVRANK'; +import TRIMMED_MEAN from './TRIMMED_MEAN'; export default { - ADD, - add: ADD, - BYRANK, - byRank: BYRANK, - BYREVRANK, - byRevRank: BYREVRANK, - CDF, - cdf: CDF, - CREATE, - create: CREATE, - INFO, - info: INFO, - MAX, - max: MAX, - MERGE, - merge: MERGE, - MIN, - min: MIN, - QUANTILE, - quantile: QUANTILE, - RANK, - rank: RANK, - RESET, - reset: RESET, - REVRANK, - revRank: REVRANK, - TRIMMED_MEAN, - trimmedMean: TRIMMED_MEAN -}; - -export interface CompressionOption { - COMPRESSION?: number; -} - -export function pushCompressionArgument( - args: RedisCommandArguments, - options?: CompressionOption -): RedisCommandArguments { - if (options?.COMPRESSION) { - args.push('COMPRESSION', options.COMPRESSION.toString()); - } - - return args; -} - -export function transformDoubleReply(reply: string): number { - switch (reply) { - case 'inf': - return Infinity; - - case '-inf': - return -Infinity; - - case 'nan': - return NaN; - - default: - return parseFloat(reply); - } -} - -export function transformDoublesReply(reply: Array): Array { - return reply.map(transformDoubleReply); -} + ADD, + add: ADD, + BYRANK, + byRank: BYRANK, + BYREVRANK, + byRevRank: BYREVRANK, + CDF, + cdf: CDF, + CREATE, + create: CREATE, + INFO, + info: INFO, + MAX, + max: MAX, + MERGE, + merge: MERGE, + MIN, + min: MIN, + QUANTILE, + quantile: QUANTILE, + RANK, + rank: RANK, + RESET, + reset: RESET, + REVRANK, + revRank: REVRANK, + TRIMMED_MEAN, + trimmedMean: TRIMMED_MEAN +} as const satisfies RedisCommands; diff --git a/packages/bloom/lib/commands/top-k/ADD.spec.ts b/packages/bloom/lib/commands/top-k/ADD.spec.ts index 149007f81d0..8f6f9300b36 100644 --- a/packages/bloom/lib/commands/top-k/ADD.spec.ts +++ b/packages/bloom/lib/commands/top-k/ADD.spec.ts @@ -1,22 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './ADD'; +import ADD from './ADD'; -describe('TOPK ADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['TOPK.ADD', 'key', 'item'] - ); - }); +describe('TOPK.ADD', () => { + it('transformArguments', () => { + assert.deepEqual( + ADD.transformArguments('key', 'item'), + ['TOPK.ADD', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.topK.add', async client => { - await client.topK.reserve('topK', 3); + testUtils.testWithClient('client.topK.add', async client => { + const [, reply] = await Promise.all([ + client.topK.reserve('topK', 3), + client.topK.add('topK', 'item') + ]); - assert.deepEqual( - await client.topK.add('topK', 'item'), - [null] - ); - - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [null]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/top-k/ADD.ts b/packages/bloom/lib/commands/top-k/ADD.ts index beee3a2206c..99982cc8e64 100644 --- a/packages/bloom/lib/commands/top-k/ADD.ts +++ b/packages/bloom/lib/commands/top-k/ADD.ts @@ -1,13 +1,11 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: string, - items: string | Array -): RedisCommandArguments { - return pushVerdictArguments(['TOPK.ADD', key], items); -} - -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, items: RedisVariadicArgument) { + return pushVariadicArguments(['TOPK.ADD', key], items); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/COUNT.spec.ts b/packages/bloom/lib/commands/top-k/COUNT.spec.ts index 318fc74c679..dce03f0e78c 100644 --- a/packages/bloom/lib/commands/top-k/COUNT.spec.ts +++ b/packages/bloom/lib/commands/top-k/COUNT.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './COUNT'; +import COUNT from './COUNT'; -describe('TOPK COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['TOPK.COUNT', 'key', 'item'] - ); - }); +describe('TOPK.COUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + COUNT.transformArguments('key', 'item'), + ['TOPK.COUNT', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.topK.count', async client => { - await client.topK.reserve('key', 3); + testUtils.testWithClient('client.topK.count', async client => { + const [, reply] = await Promise.all([ + client.topK.reserve('key', 3), + client.topK.count('key', 'item') + ]); - assert.deepEqual( - await client.topK.count('key', 'item'), - [0] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [0]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/top-k/COUNT.ts b/packages/bloom/lib/commands/top-k/COUNT.ts index fc8cf557dca..7e3ccc6dc4c 100644 --- a/packages/bloom/lib/commands/top-k/COUNT.ts +++ b/packages/bloom/lib/commands/top-k/COUNT.ts @@ -1,15 +1,11 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: string, - items: string | Array -): RedisCommandArguments { - return pushVerdictArguments(['TOPK.COUNT', key], items); -} - -export declare function transformReply(): Array; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, items: RedisVariadicArgument) { + return pushVariadicArguments(['TOPK.COUNT', key], items); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/INCRBY.spec.ts b/packages/bloom/lib/commands/top-k/INCRBY.spec.ts index b23ca6e0ed1..aa7032a9a02 100644 --- a/packages/bloom/lib/commands/top-k/INCRBY.spec.ts +++ b/packages/bloom/lib/commands/top-k/INCRBY.spec.ts @@ -1,42 +1,42 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INCRBY'; +import INCRBY from './INCRBY'; -describe('TOPK INCRBY', () => { - describe('transformArguments', () => { - it('single item', () => { - assert.deepEqual( - transformArguments('key', { - item: 'item', - incrementBy: 1 - }), - ['TOPK.INCRBY', 'key', 'item', '1'] - ); - }); +describe('TOPK.INCRBY', () => { + describe('transformArguments', () => { + it('single item', () => { + assert.deepEqual( + INCRBY.transformArguments('key', { + item: 'item', + incrementBy: 1 + }), + ['TOPK.INCRBY', 'key', 'item', '1'] + ); + }); - it('multiple items', () => { - assert.deepEqual( - transformArguments('key', [{ - item: 'a', - incrementBy: 1 - }, { - item: 'b', - incrementBy: 2 - }]), - ['TOPK.INCRBY', 'key', 'a', '1', 'b', '2'] - ); - }); + it('multiple items', () => { + assert.deepEqual( + INCRBY.transformArguments('key', [{ + item: 'a', + incrementBy: 1 + }, { + item: 'b', + incrementBy: 2 + }]), + ['TOPK.INCRBY', 'key', 'a', '1', 'b', '2'] + ); }); + }); - testUtils.testWithClient('client.topK.incrby', async client => { - await client.topK.reserve('key', 5); + testUtils.testWithClient('client.topK.incrby', async client => { + const [, reply] = await Promise.all([ + client.topK.reserve('key', 5), + client.topK.incrBy('key', { + item: 'item', + incrementBy: 1 + }) + ]); - assert.deepEqual( - await client.topK.incrBy('key', { - item: 'item', - incrementBy: 1 - }), - [null] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [null]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/top-k/INCRBY.ts b/packages/bloom/lib/commands/top-k/INCRBY.ts index 2533cb05594..16decf44dca 100644 --- a/packages/bloom/lib/commands/top-k/INCRBY.ts +++ b/packages/bloom/lib/commands/top-k/INCRBY.ts @@ -1,29 +1,32 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -interface IncrByItem { - item: string; - incrementBy: number; +export interface TopKIncrByItem { + item: string; + incrementBy: number; } -export function transformArguments( - key: string, - items: IncrByItem | Array -): Array { +function pushIncrByItem(args: Array, { item, incrementBy }: TopKIncrByItem) { + args.push(item, incrementBy.toString()); +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + items: TopKIncrByItem | Array + ) { const args = ['TOPK.INCRBY', key]; if (Array.isArray(items)) { - for (const item of items) { - pushIncrByItem(args, item); - } + for (const item of items) { + pushIncrByItem(args, item); + } } else { - pushIncrByItem(args, items); + pushIncrByItem(args, items); } return args; -} - -function pushIncrByItem(args: Array, { item, incrementBy }: IncrByItem): void { - args.push(item, incrementBy.toString()); -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/INFO.spec.ts b/packages/bloom/lib/commands/top-k/INFO.spec.ts index 2741a58a8ba..8e17829a2a6 100644 --- a/packages/bloom/lib/commands/top-k/INFO.spec.ts +++ b/packages/bloom/lib/commands/top-k/INFO.spec.ts @@ -1,23 +1,26 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './INFO'; +import INFO from './INFO'; describe('TOPK INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TOPK.INFO', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + INFO.transformArguments('key'), + ['TOPK.INFO', 'key'] + ); + }); - testUtils.testWithClient('client.topK.info', async client => { - await client.topK.reserve('key', 3); + testUtils.testWithClient('client.topK.info', async client => { + const k = 3, + [, reply] = await Promise.all([ + client.topK.reserve('key', 3), + client.topK.info('key') + ]); - const info = await client.topK.info('key'); - assert.equal(typeof info, 'object'); - assert.equal(info.k, 3); - assert.equal(typeof info.width, 'number'); - assert.equal(typeof info.depth, 'number'); - assert.equal(typeof info.decay, 'number'); - }, GLOBAL.SERVERS.OPEN); + assert.equal(typeof reply, 'object'); + assert.equal(reply.k, k); + assert.equal(typeof reply.width, 'number'); + assert.equal(typeof reply.depth, 'number'); + assert.equal(typeof reply.decay, 'number'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/top-k/INFO.ts b/packages/bloom/lib/commands/top-k/INFO.ts index 8c9e8d432b3..e6f55ac2c1b 100644 --- a/packages/bloom/lib/commands/top-k/INFO.ts +++ b/packages/bloom/lib/commands/top-k/INFO.ts @@ -1,34 +1,26 @@ -export const FIRST_KEY_INDEX = 1; +import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { RedisArgument, TuplesToMapReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { transformInfoV2Reply } from '../bloom'; -export const IS_READ_ONLY = true; +export type TopKInfoReplyMap = TuplesToMapReply<[ + [SimpleStringReply<'k'>, NumberReply], + [SimpleStringReply<'width'>, NumberReply], + [SimpleStringReply<'depth'>, NumberReply], + [SimpleStringReply<'decay'>, DoubleReply] +]>; -export function transformArguments(key: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['TOPK.INFO', key]; -} + }, + transformReply: { + 2: (reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping): TopKInfoReplyMap => { + reply[7] = transformDoubleReply[2](reply[7], preserve, typeMapping) as any; -export type InfoRawReply = [ - _: string, - k: number, - _: string, - width: number, - _: string, - depth: number, - _: string, - decay: string -]; - -export interface InfoReply { - k: number, - width: number; - depth: number; - decay: number; -} - -export function transformReply(reply: InfoRawReply): InfoReply { - return { - k: reply[1], - width: reply[3], - depth: reply[5], - decay: Number(reply[7]) - }; -} + return transformInfoV2Reply(reply, typeMapping); + }, + 3: undefined as unknown as () => TopKInfoReplyMap + } +} as const satisfies Command diff --git a/packages/bloom/lib/commands/top-k/LIST.spec.ts b/packages/bloom/lib/commands/top-k/LIST.spec.ts index 709ac7ffc39..7ab96182bbe 100644 --- a/packages/bloom/lib/commands/top-k/LIST.spec.ts +++ b/packages/bloom/lib/commands/top-k/LIST.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './LIST'; +import LIST from './LIST'; -describe('TOPK LIST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TOPK.LIST', 'key'] - ); - }); +describe('TOPK.LIST', () => { + it('transformArguments', () => { + assert.deepEqual( + LIST.transformArguments('key'), + ['TOPK.LIST', 'key'] + ); + }); - testUtils.testWithClient('client.topK.list', async client => { - await client.topK.reserve('key', 3); + testUtils.testWithClient('client.topK.list', async client => { + const [, reply] = await Promise.all([ + client.topK.reserve('key', 3), + client.topK.list('key') + ]); - assert.deepEqual( - await client.topK.list('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, []); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/top-k/LIST.ts b/packages/bloom/lib/commands/top-k/LIST.ts index 8837b86f830..26345b72462 100644 --- a/packages/bloom/lib/commands/top-k/LIST.ts +++ b/packages/bloom/lib/commands/top-k/LIST.ts @@ -1,9 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['TOPK.LIST', key]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts index 1e55239c243..862d17eb3e3 100644 --- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts +++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts @@ -1,30 +1,27 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './LIST_WITHCOUNT'; +import LIST_WITHCOUNT from './LIST_WITHCOUNT'; -describe('TOPK LIST WITHCOUNT', () => { - testUtils.isVersionGreaterThanHook([2, 2, 9]); - - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TOPK.LIST', 'key', 'WITHCOUNT'] - ); - }); +describe('TOPK.LIST WITHCOUNT', () => { + testUtils.isVersionGreaterThanHook([2, 2, 9]); - testUtils.testWithClient('client.topK.listWithCount', async client => { - const [,, list] = await Promise.all([ - client.topK.reserve('key', 3), - client.topK.add('key', 'item'), - client.topK.listWithCount('key') - ]); + it('transformArguments', () => { + assert.deepEqual( + LIST_WITHCOUNT.transformArguments('key'), + ['TOPK.LIST', 'key', 'WITHCOUNT'] + ); + }); - assert.deepEqual( - list, - [{ - item: 'item', - count: 1 - }] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.topK.listWithCount', async client => { + const [, , list] = await Promise.all([ + client.topK.reserve('key', 3), + client.topK.add('key', 'item'), + client.topK.listWithCount('key') + ]); + + assert.deepEqual(list, [{ + item: 'item', + count: 1 + }]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts index 47b7d3848ed..d26936fd3c7 100644 --- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts +++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts @@ -1,26 +1,24 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['TOPK.LIST', key, 'WITHCOUNT']; -} - -type ListWithCountRawReply = Array; - -type ListWithCountReply = Array<{ - item: string, - count: number -}>; - -export function transformReply(rawReply: ListWithCountRawReply): ListWithCountReply { - const reply: ListWithCountReply = []; + }, + transformReply(rawReply: UnwrapReply>) { + const reply: Array<{ + item: BlobStringReply; + count: NumberReply; + }> = []; + for (let i = 0; i < rawReply.length; i++) { - reply.push({ - item: rawReply[i] as string, - count: rawReply[++i] as number - }); + reply.push({ + item: rawReply[i] as BlobStringReply, + count: rawReply[++i] as NumberReply + }); } return reply; -} \ No newline at end of file + } +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/QUERY.spec.ts b/packages/bloom/lib/commands/top-k/QUERY.spec.ts index ada9e7e2e39..d5ecfebb6c6 100644 --- a/packages/bloom/lib/commands/top-k/QUERY.spec.ts +++ b/packages/bloom/lib/commands/top-k/QUERY.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './QUERY'; +import QUERY from './QUERY'; -describe('TOPK QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'item'), - ['TOPK.QUERY', 'key', 'item'] - ); - }); +describe('TOPK.QUERY', () => { + it('transformArguments', () => { + assert.deepEqual( + QUERY.transformArguments('key', 'item'), + ['TOPK.QUERY', 'key', 'item'] + ); + }); - testUtils.testWithClient('client.cms.query', async client => { - await client.topK.reserve('key', 3); + testUtils.testWithClient('client.topK.query', async client => { + const [, reply] = await Promise.all([ + client.topK.reserve('key', 3), + client.topK.query('key', 'item') + ]); - assert.deepEqual( - await client.topK.query('key', 'item'), - [0] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [false]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/top-k/QUERY.ts b/packages/bloom/lib/commands/top-k/QUERY.ts index 94943a26fd7..5529d4ab838 100644 --- a/packages/bloom/lib/commands/top-k/QUERY.ts +++ b/packages/bloom/lib/commands/top-k/QUERY.ts @@ -1,15 +1,11 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: string, - items: string | Array -): RedisCommandArguments { - return pushVerdictArguments(['TOPK.QUERY', key], items); -} - -export declare function transformReply(): Array; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments, transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, items: RedisVariadicArgument) { + return pushVariadicArguments(['TOPK.QUERY', key], items); + }, + transformReply: transformBooleanArrayReply +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/RESERVE.spec.ts b/packages/bloom/lib/commands/top-k/RESERVE.spec.ts index 54600c0e4f5..39d8fb7efc6 100644 --- a/packages/bloom/lib/commands/top-k/RESERVE.spec.ts +++ b/packages/bloom/lib/commands/top-k/RESERVE.spec.ts @@ -1,32 +1,32 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; -import { transformArguments } from './RESERVE'; +import RESERVE from './RESERVE'; -describe('TOPK RESERVE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('topK', 3), - ['TOPK.RESERVE', 'topK', '3'] - ); - }); +describe('TOPK.RESERVE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + RESERVE.transformArguments('topK', 3), + ['TOPK.RESERVE', 'topK', '3'] + ); + }); - it('with options', () => { - assert.deepEqual( - transformArguments('topK', 3, { - width: 8, - depth: 7, - decay: 0.9 - }), - ['TOPK.RESERVE', 'topK', '3', '8', '7', '0.9'] - ); - }); + it('with options', () => { + assert.deepEqual( + RESERVE.transformArguments('topK', 3, { + width: 8, + depth: 7, + decay: 0.9 + }), + ['TOPK.RESERVE', 'topK', '3', '8', '7', '0.9'] + ); }); + }); - testUtils.testWithClient('client.topK.reserve', async client => { - assert.equal( - await client.topK.reserve('topK', 3), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.topK.reserve', async client => { + assert.equal( + await client.topK.reserve('topK', 3), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/bloom/lib/commands/top-k/RESERVE.ts b/packages/bloom/lib/commands/top-k/RESERVE.ts index 350d4cd8339..12671728fea 100644 --- a/packages/bloom/lib/commands/top-k/RESERVE.ts +++ b/packages/bloom/lib/commands/top-k/RESERVE.ts @@ -1,29 +1,26 @@ -export const FIRST_KEY_INDEX = 1; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -interface ReserveOptions { - width: number; - depth: number; - decay: number; +export interface TopKReserveOptions { + width: number; + depth: number; + decay: number; } -export function transformArguments( - key: string, - topK: number, - options?: ReserveOptions -): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, topK: number, options?: TopKReserveOptions) { const args = ['TOPK.RESERVE', key, topK.toString()]; if (options) { - args.push( - options.width.toString(), - options.depth.toString(), - options.decay.toString() - ); + args.push( + options.width.toString(), + options.depth.toString(), + options.decay.toString() + ); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/index.ts b/packages/bloom/lib/commands/top-k/index.ts index 750c91dfa88..fb5de543cab 100644 --- a/packages/bloom/lib/commands/top-k/index.ts +++ b/packages/bloom/lib/commands/top-k/index.ts @@ -1,27 +1,28 @@ -import * as ADD from './ADD'; -import * as COUNT from './COUNT'; -import * as INCRBY from './INCRBY'; -import * as INFO from './INFO'; -import * as LIST_WITHCOUNT from './LIST_WITHCOUNT'; -import * as LIST from './LIST'; -import * as QUERY from './QUERY'; -import * as RESERVE from './RESERVE'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import ADD from './ADD'; +import COUNT from './COUNT'; +import INCRBY from './INCRBY'; +import INFO from './INFO'; +import LIST_WITHCOUNT from './LIST_WITHCOUNT'; +import LIST from './LIST'; +import QUERY from './QUERY'; +import RESERVE from './RESERVE'; export default { - ADD, - add: ADD, - COUNT, - count: COUNT, - INCRBY, - incrBy: INCRBY, - INFO, - info: INFO, - LIST_WITHCOUNT, - listWithCount: LIST_WITHCOUNT, - LIST, - list: LIST, - QUERY, - query: QUERY, - RESERVE, - reserve: RESERVE -}; + ADD, + add: ADD, + COUNT, + count: COUNT, + INCRBY, + incrBy: INCRBY, + INFO, + info: INFO, + LIST_WITHCOUNT, + listWithCount: LIST_WITHCOUNT, + LIST, + list: LIST, + QUERY, + query: QUERY, + RESERVE, + reserve: RESERVE +} as const satisfies RedisCommands; diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index a2e059b3b99..1291054e802 100644 --- a/packages/bloom/lib/test-utils.ts +++ b/packages/bloom/lib/test-utils.ts @@ -2,18 +2,18 @@ import TestUtils from '@redis/test-utils'; import RedisBloomModules from '.'; export default new TestUtils({ - dockerImageName: 'redislabs/rebloom', - dockerImageVersionArgument: 'redisbloom-version', - defaultDockerVersion: 'edge' + dockerImageName: 'redis/redis-stack', + dockerImageVersionArgument: 'redisbloom-version', + defaultDockerVersion: '7.4.0-v1' }); export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: ['--loadmodule /usr/lib/redis/modules/redisbloom.so'], - clientOptions: { - modules: RedisBloomModules - } - } + SERVERS: { + OPEN: { + serverArguments: [], + clientOptions: { + modules: RedisBloomModules + } } + } }; diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 8a9d9f7a87a..850ad802a5d 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,30 +1,24 @@ { "name": "@redis/bloom", - "version": "1.2.0", + "version": "2.0.0-next.3", "license": "MIT", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/lib/index.js", + "types": "./dist/lib/index.d.ts", "files": [ - "dist/" + "dist/", + "!dist/tsconfig.tsbuildinfo" ], "scripts": { - "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'", - "build": "tsc", - "documentation": "typedoc" + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" }, "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "repository": { "type": "git", diff --git a/packages/client/.eslintrc.json b/packages/client/.eslintrc.json deleted file mode 100644 index 4536bc31338..00000000000 --- a/packages/client/.eslintrc.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "root": true, - "parser": "@typescript-eslint/parser", - "plugins": [ - "@typescript-eslint" - ], - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended" - ], - "rules": { - "semi": [2, "always"] - } - } diff --git a/packages/client/.npmignore b/packages/client/.npmignore deleted file mode 100644 index d064e1d0db6..00000000000 --- a/packages/client/.npmignore +++ /dev/null @@ -1,10 +0,0 @@ -.nyc_output/ -coverage/ -documentation/ -lib/ -.eslintrc.json -.nycrc.json -.release-it.json -dump.rdb -index.ts -tsconfig.json diff --git a/packages/client/.release-it.json b/packages/client/.release-it.json index 035124348ca..3ae247ad371 100644 --- a/packages/client/.release-it.json +++ b/packages/client/.release-it.json @@ -5,6 +5,7 @@ "tagAnnotation": "Release ${tagName}" }, "npm": { + "versionArgs": ["--workspaces-update=false"], "publishArgs": ["--access", "public"] } } diff --git a/packages/client/index.ts b/packages/client/index.ts index 8b21c5d5a32..56cdf703ca3 100644 --- a/packages/client/index.ts +++ b/packages/client/index.ts @@ -1,24 +1,27 @@ -import RedisClient from './lib/client'; -import RedisCluster from './lib/cluster'; - -export { RedisClientType, RedisClientOptions } from './lib/client'; - -export { RedisModules, RedisFunctions, RedisScripts } from './lib/commands'; +export { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping/*, CommandPolicies*/, RedisArgument } from './lib/RESP/types'; +export { RESP_TYPES } from './lib/RESP/decoder'; +export { VerbatimString } from './lib/RESP/verbatim-string'; +export { defineScript } from './lib/lua-script'; +// export * from './lib/errors'; +import RedisClient, { RedisClientOptions, RedisClientType } from './lib/client'; +export { RedisClientOptions, RedisClientType }; export const createClient = RedisClient.create; -export const commandOptions = RedisClient.commandOptions; - -export { RedisClusterType, RedisClusterOptions } from './lib/cluster'; +import { RedisClientPool, RedisPoolOptions, RedisClientPoolType } from './lib/client/pool'; +export { RedisClientPoolType, RedisPoolOptions }; +export const createClientPool = RedisClientPool.create; +import RedisCluster, { RedisClusterOptions, RedisClusterType } from './lib/cluster'; +export { RedisClusterType, RedisClusterOptions }; export const createCluster = RedisCluster.create; -export { defineScript } from './lib/lua-script'; - -export * from './lib/errors'; +import RedisSentinel from './lib/sentinel'; +export { RedisSentinelOptions, RedisSentinelType } from './lib/sentinel/types'; +export const createSentinel = RedisSentinel.create; -export { GeoReplyWith } from './lib/commands/generic-transformers'; +// export { GeoReplyWith } from './lib/commands/generic-transformers'; -export { SetOptions } from './lib/commands/SET'; +// export { SetOptions } from './lib/commands/SET'; -export { RedisFlushModes } from './lib/commands/FLUSHALL'; +// export { RedisFlushModes } from './lib/commands/FLUSHALL'; diff --git a/packages/client/lib/RESP/decoder.spec.ts b/packages/client/lib/RESP/decoder.spec.ts new file mode 100644 index 00000000000..c034815c9cd --- /dev/null +++ b/packages/client/lib/RESP/decoder.spec.ts @@ -0,0 +1,426 @@ +import { strict as assert } from 'node:assert'; +import { SinonSpy, spy } from 'sinon'; +import { Decoder, RESP_TYPES } from './decoder'; +import { BlobError, SimpleError } from '../errors'; +import { TypeMapping } from './types'; +import { VerbatimString } from './verbatim-string'; + +interface Test { + toWrite: Buffer; + typeMapping?: TypeMapping; + replies?: Array; + errorReplies?: Array; + pushReplies?: Array; +} + +function test(name: string, config: Test) { + describe(name, () => { + it('single chunk', () => { + const setup = setupTest(config); + setup.decoder.write(config.toWrite); + assertSpiesCalls(config, setup); + }); + + it('byte by byte', () => { + const setup = setupTest(config); + for (let i = 0; i < config.toWrite.length; i++) { + setup.decoder.write(config.toWrite.subarray(i, i + 1)); + } + assertSpiesCalls(config, setup); + }); + }) +} + +function setupTest(config: Test) { + const onReplySpy = spy(), + onErrorReplySpy = spy(), + onPushSpy = spy(); + + return { + decoder: new Decoder({ + getTypeMapping: () => config.typeMapping ?? {}, + onReply: onReplySpy, + onErrorReply: onErrorReplySpy, + onPush: onPushSpy + }), + onReplySpy, + onErrorReplySpy, + onPushSpy + }; +} + +function assertSpiesCalls(config: Test, spies: ReturnType) { + assertSpyCalls(spies.onReplySpy, config.replies); + assertSpyCalls(spies.onErrorReplySpy, config.errorReplies); + assertSpyCalls(spies.onPushSpy, config.pushReplies); +} + +function assertSpyCalls(spy: SinonSpy, replies?: Array) { + if (!replies) { + assert.equal(spy.callCount, 0); + return; + } + + assert.equal(spy.callCount, replies.length); + for (const [i, reply] of replies.entries()) { + assert.deepEqual( + spy.getCall(i).args, + [reply] + ); + } +} + +describe('RESP Decoder', () => { + test('Null', { + toWrite: Buffer.from('_\r\n'), + replies: [null] + }); + + describe('Boolean', () => { + test('true', { + toWrite: Buffer.from('#t\r\n'), + replies: [true] + }); + + test('false', { + toWrite: Buffer.from('#f\r\n'), + replies: [false] + }); + }); + + describe('Number', () => { + test('0', { + toWrite: Buffer.from(':0\r\n'), + replies: [0] + }); + + test('1', { + toWrite: Buffer.from(':+1\r\n'), + replies: [1] + }); + + test('+1', { + toWrite: Buffer.from(':+1\r\n'), + replies: [1] + }); + + test('-1', { + toWrite: Buffer.from(':-1\r\n'), + replies: [-1] + }); + + test('1 as string', { + typeMapping: { + [RESP_TYPES.NUMBER]: String + }, + toWrite: Buffer.from(':1\r\n'), + replies: ['1'] + }); + }); + + describe('BigNumber', () => { + test('0', { + toWrite: Buffer.from('(0\r\n'), + replies: [0n] + }); + + test('1', { + toWrite: Buffer.from('(1\r\n'), + replies: [1n] + }); + + test('+1', { + toWrite: Buffer.from('(+1\r\n'), + replies: [1n] + }); + + test('-1', { + toWrite: Buffer.from('(-1\r\n'), + replies: [-1n] + }); + + test('1 as string', { + typeMapping: { + [RESP_TYPES.BIG_NUMBER]: String + }, + toWrite: Buffer.from('(1\r\n'), + replies: ['1'] + }); + }); + + describe('Double', () => { + test('0', { + toWrite: Buffer.from(',0\r\n'), + replies: [0] + }); + + test('1', { + toWrite: Buffer.from(',1\r\n'), + replies: [1] + }); + + test('+1', { + toWrite: Buffer.from(',+1\r\n'), + replies: [1] + }); + + test('-1', { + toWrite: Buffer.from(',-1\r\n'), + replies: [-1] + }); + + test('1.1', { + toWrite: Buffer.from(',1.1\r\n'), + replies: [1.1] + }); + + test('nan', { + toWrite: Buffer.from(',nan\r\n'), + replies: [NaN] + }); + + test('inf', { + toWrite: Buffer.from(',inf\r\n'), + replies: [Infinity] + }); + + test('+inf', { + toWrite: Buffer.from(',+inf\r\n'), + replies: [Infinity] + }); + + test('-inf', { + toWrite: Buffer.from(',-inf\r\n'), + replies: [-Infinity] + }); + + test('1e1', { + toWrite: Buffer.from(',1e1\r\n'), + replies: [1e1] + }); + + test('-1.1E+1', { + toWrite: Buffer.from(',-1.1E+1\r\n'), + replies: [-1.1E+1] + }); + + test('1 as string', { + typeMapping: { + [RESP_TYPES.DOUBLE]: String + }, + toWrite: Buffer.from(',1\r\n'), + replies: ['1'] + }); + }); + + describe('SimpleString', () => { + test("'OK'", { + toWrite: Buffer.from('+OK\r\n'), + replies: ['OK'] + }); + + test("'OK' as Buffer", { + typeMapping: { + [RESP_TYPES.SIMPLE_STRING]: Buffer + }, + toWrite: Buffer.from('+OK\r\n'), + replies: [Buffer.from('OK')] + }); + }); + + describe('BlobString', () => { + test("''", { + toWrite: Buffer.from('$0\r\n\r\n'), + replies: [''] + }); + + test("'1234567890'", { + toWrite: Buffer.from('$10\r\n1234567890\r\n'), + replies: ['1234567890'] + }); + + test('null (RESP2 backwards compatibility)', { + toWrite: Buffer.from('$-1\r\n'), + replies: [null] + }); + + test("'OK' as Buffer", { + typeMapping: { + [RESP_TYPES.BLOB_STRING]: Buffer + }, + toWrite: Buffer.from('$2\r\nOK\r\n'), + replies: [Buffer.from('OK')] + }); + }); + + describe('VerbatimString', () => { + test("''", { + toWrite: Buffer.from('=4\r\ntxt:\r\n'), + replies: [''] + }); + + test("'123456'", { + toWrite: Buffer.from('=10\r\ntxt:123456\r\n'), + replies: ['123456'] + }); + + test("'OK' as VerbatimString", { + typeMapping: { + [RESP_TYPES.VERBATIM_STRING]: VerbatimString + }, + toWrite: Buffer.from('=6\r\ntxt:OK\r\n'), + replies: [new VerbatimString('txt', 'OK')] + }); + + test("'OK' as Buffer", { + typeMapping: { + [RESP_TYPES.VERBATIM_STRING]: Buffer + }, + toWrite: Buffer.from('=6\r\ntxt:OK\r\n'), + replies: [Buffer.from('OK')] + }); + }); + + test('SimpleError', { + toWrite: Buffer.from('-ERROR\r\n'), + errorReplies: [new SimpleError('ERROR')] + }); + + test('BlobError', { + toWrite: Buffer.from('!5\r\nERROR\r\n'), + errorReplies: [new BlobError('ERROR')] + }); + + describe('Array', () => { + test('[]', { + toWrite: Buffer.from('*0\r\n'), + replies: [[]] + }); + + test('[0..9]', { + toWrite: Buffer.from(`*10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`), + replies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + }); + + test('with all types', { + toWrite: Buffer.from([ + '*13\r\n', + '_\r\n', + '#f\r\n', + ':0\r\n', + '(0\r\n', + ',0\r\n', + '+\r\n', + '$0\r\n\r\n', + '=4\r\ntxt:\r\n', + '-\r\n', + '!0\r\n\r\n', + '*0\r\n', + '~0\r\n', + '%0\r\n' + ].join('')), + replies: [[ + null, + false, + 0, + 0n, + 0, + '', + '', + '', + new SimpleError(''), + new BlobError(''), + [], + [], + Object.create(null) + ]] + }); + + test('null (RESP2 backwards compatibility)', { + toWrite: Buffer.from('*-1\r\n'), + replies: [null] + }); + }); + + describe('Set', () => { + test('empty', { + toWrite: Buffer.from('~0\r\n'), + replies: [[]] + }); + + test('of 0..9', { + toWrite: Buffer.from(`~10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`), + replies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + }); + + test('0..9 as Set', { + typeMapping: { + [RESP_TYPES.SET]: Set + }, + toWrite: Buffer.from(`~10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`), + replies: [new Set([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])] + }); + }); + + describe('Map', () => { + test('{}', { + toWrite: Buffer.from('%0\r\n'), + replies: [Object.create(null)] + }); + + test("{ '0'..'9': }", { + toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`), + replies: [Object.create(null, { + 0: { value: '0', enumerable: true }, + 1: { value: '1', enumerable: true }, + 2: { value: '2', enumerable: true }, + 3: { value: '3', enumerable: true }, + 4: { value: '4', enumerable: true }, + 5: { value: '5', enumerable: true }, + 6: { value: '6', enumerable: true }, + 7: { value: '7', enumerable: true }, + 8: { value: '8', enumerable: true }, + 9: { value: '9', enumerable: true } + })] + }); + + test("{ '0'..'9': } as Map", { + typeMapping: { + [RESP_TYPES.MAP]: Map + }, + toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`), + replies: [new Map([ + ['0', '0'], + ['1', '1'], + ['2', '2'], + ['3', '3'], + ['4', '4'], + ['5', '5'], + ['6', '6'], + ['7', '7'], + ['8', '8'], + ['9', '9'] + ])] + }); + + test("{ '0'..'9': } as Array", { + typeMapping: { + [RESP_TYPES.MAP]: Array + }, + toWrite: Buffer.from(`%10\r\n+0\r\n+0\r\n+1\r\n+1\r\n+2\r\n+2\r\n+3\r\n+3\r\n+4\r\n+4\r\n+5\r\n+5\r\n+6\r\n+6\r\n+7\r\n+7\r\n+8\r\n+8\r\n+9\r\n+9\r\n`), + replies: [['0', '0', '1', '1', '2', '2', '3', '3', '4', '4', '5', '5', '6', '6', '7', '7', '8', '8', '9', '9']] + }); + }); + + describe('Push', () => { + test('[]', { + toWrite: Buffer.from('>0\r\n'), + pushReplies: [[]] + }); + + test('[0..9]', { + toWrite: Buffer.from(`>10\r\n:0\r\n:1\r\n:2\r\n:3\r\n:4\r\n:5\r\n:6\r\n:7\r\n:8\r\n:9\r\n`), + pushReplies: [[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]] + }); + }); +}); diff --git a/packages/client/lib/RESP/decoder.ts b/packages/client/lib/RESP/decoder.ts new file mode 100644 index 00000000000..2485ea23b37 --- /dev/null +++ b/packages/client/lib/RESP/decoder.ts @@ -0,0 +1,1178 @@ +// @ts-nocheck +import { VerbatimString } from './verbatim-string'; +import { SimpleError, BlobError, ErrorReply } from '../errors'; +import { TypeMapping } from './types'; + +// https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md +export const RESP_TYPES = { + NULL: 95, // _ + BOOLEAN: 35, // # + NUMBER: 58, // : + BIG_NUMBER: 40, // ( + DOUBLE: 44, // , + SIMPLE_STRING: 43, // + + BLOB_STRING: 36, // $ + VERBATIM_STRING: 61, // = + SIMPLE_ERROR: 45, // - + BLOB_ERROR: 33, // ! + ARRAY: 42, // * + SET: 126, // ~ + MAP: 37, // % + PUSH: 62 // > +} as const; + +const ASCII = { + '\r': 13, + 't': 116, + '+': 43, + '-': 45, + '0': 48, + '.': 46, + 'i': 105, + 'n': 110, + 'E': 69, + 'e': 101 +} as const; + +export const PUSH_TYPE_MAPPING = { + [RESP_TYPES.BLOB_STRING]: Buffer +}; + +// this was written with performance in mind, so it's not very readable... sorry :( + +interface DecoderOptions { + onReply(reply: any): unknown; + onErrorReply(err: ErrorReply): unknown; + onPush(push: Array): unknown; + getTypeMapping(): TypeMapping; +} + +export class Decoder { + onReply; + onErrorReply; + onPush; + getTypeMapping; + #cursor = 0; + #next; + + constructor(config: DecoderOptions) { + this.onReply = config.onReply; + this.onErrorReply = config.onErrorReply; + this.onPush = config.onPush; + this.getTypeMapping = config.getTypeMapping; + } + + reset() { + this.#cursor = 0; + this.#next = undefined; + } + + write(chunk) { + if (this.#cursor >= chunk.length) { + this.#cursor -= chunk.length; + return; + } + + if (this.#next) { + if (this.#next(chunk) || this.#cursor >= chunk.length) { + this.#cursor -= chunk.length; + return; + } + } + + do { + const type = chunk[this.#cursor]; + if (++this.#cursor === chunk.length) { + this.#next = this.#continueDecodeTypeValue.bind(this, type); + break; + } + + if (this.#decodeTypeValue(type, chunk)) { + break; + } + } while (this.#cursor < chunk.length); + this.#cursor -= chunk.length; + } + + #continueDecodeTypeValue(type, chunk) { + this.#next = undefined; + return this.#decodeTypeValue(type, chunk); + } + + #decodeTypeValue(type, chunk) { + switch (type) { + case RESP_TYPES.NULL: + this.onReply(this.#decodeNull()); + return false; + + case RESP_TYPES.BOOLEAN: + return this.#handleDecodedValue( + this.onReply, + this.#decodeBoolean(chunk) + ); + + case RESP_TYPES.NUMBER: + return this.#handleDecodedValue( + this.onReply, + this.#decodeNumber( + this.getTypeMapping()[RESP_TYPES.NUMBER], + chunk + ) + ); + + case RESP_TYPES.BIG_NUMBER: + return this.#handleDecodedValue( + this.onReply, + this.#decodeBigNumber( + this.getTypeMapping()[RESP_TYPES.BIG_NUMBER], + chunk + ) + ); + + case RESP_TYPES.DOUBLE: + return this.#handleDecodedValue( + this.onReply, + this.#decodeDouble( + this.getTypeMapping()[RESP_TYPES.DOUBLE], + chunk + ) + ); + + case RESP_TYPES.SIMPLE_STRING: + return this.#handleDecodedValue( + this.onReply, + this.#decodeSimpleString( + this.getTypeMapping()[RESP_TYPES.SIMPLE_STRING], + chunk + ) + ); + + case RESP_TYPES.BLOB_STRING: + return this.#handleDecodedValue( + this.onReply, + this.#decodeBlobString( + this.getTypeMapping()[RESP_TYPES.BLOB_STRING], + chunk + ) + ); + + case RESP_TYPES.VERBATIM_STRING: + return this.#handleDecodedValue( + this.onReply, + this.#decodeVerbatimString( + this.getTypeMapping()[RESP_TYPES.VERBATIM_STRING], + chunk + ) + ); + + case RESP_TYPES.SIMPLE_ERROR: + return this.#handleDecodedValue( + this.onErrorReply, + this.#decodeSimpleError(chunk) + ); + + case RESP_TYPES.BLOB_ERROR: + return this.#handleDecodedValue( + this.onErrorReply, + this.#decodeBlobError(chunk) + ); + + case RESP_TYPES.ARRAY: + return this.#handleDecodedValue( + this.onReply, + this.#decodeArray(this.getTypeMapping(), chunk) + ); + + case RESP_TYPES.SET: + return this.#handleDecodedValue( + this.onReply, + this.#decodeSet(this.getTypeMapping(), chunk) + ); + + case RESP_TYPES.MAP: + return this.#handleDecodedValue( + this.onReply, + this.#decodeMap(this.getTypeMapping(), chunk) + ); + + case RESP_TYPES.PUSH: + return this.#handleDecodedValue( + this.onPush, + this.#decodeArray(PUSH_TYPE_MAPPING, chunk) + ); + + default: + throw new Error(`Unknown RESP type ${type} "${String.fromCharCode(type)}"`); + } + } + + #handleDecodedValue(cb, value) { + if (typeof value === 'function') { + this.#next = this.#continueDecodeValue.bind(this, cb, value); + return true; + } + + cb(value); + return false; + } + + #continueDecodeValue(cb, next, chunk) { + this.#next = undefined; + return this.#handleDecodedValue(cb, next(chunk)); + } + + #decodeNull() { + this.#cursor += 2; // skip \r\n + return null; + } + + #decodeBoolean(chunk) { + const boolean = chunk[this.#cursor] === ASCII.t; + this.#cursor += 3; // skip {t | f}\r\n + return boolean; + } + + #decodeNumber(type, chunk) { + if (type === String) { + return this.#decodeSimpleString(String, chunk); + } + + switch (chunk[this.#cursor]) { + case ASCII['+']: + return this.#maybeDecodeNumberValue(false, chunk); + + case ASCII['-']: + return this.#maybeDecodeNumberValue(true, chunk); + + default: + return this.#decodeNumberValue( + false, + this.#decodeUnsingedNumber.bind(this, 0), + chunk + ); + } + } + + #maybeDecodeNumberValue(isNegative, chunk) { + const cb = this.#decodeUnsingedNumber.bind(this, 0); + return ++this.#cursor === chunk.length ? + this.#decodeNumberValue.bind(this, isNegative, cb) : + this.#decodeNumberValue(isNegative, cb, chunk); + } + + #decodeNumberValue(isNegative, numberCb, chunk) { + const number = numberCb(chunk); + return typeof number === 'function' ? + this.#decodeNumberValue.bind(this, isNegative, number) : + isNegative ? -number : number; + } + + #decodeUnsingedNumber(number, chunk) { + let cursor = this.#cursor; + do { + const byte = chunk[cursor]; + if (byte === ASCII['\r']) { + this.#cursor = cursor + 2; // skip \r\n + return number; + } + number = number * 10 + byte - ASCII['0']; + } while (++cursor < chunk.length); + + this.#cursor = cursor; + return this.#decodeUnsingedNumber.bind(this, number); + } + + #decodeBigNumber(type, chunk) { + if (type === String) { + return this.#decodeSimpleString(String, chunk); + } + + switch (chunk[this.#cursor]) { + case ASCII['+']: + return this.#maybeDecodeBigNumberValue(false, chunk); + + case ASCII['-']: + return this.#maybeDecodeBigNumberValue(true, chunk); + + default: + return this.#decodeBigNumberValue( + false, + this.#decodeUnsingedBigNumber.bind(this, 0n), + chunk + ); + } + } + + #maybeDecodeBigNumberValue(isNegative, chunk) { + const cb = this.#decodeUnsingedBigNumber.bind(this, 0n); + return ++this.#cursor === chunk.length ? + this.#decodeBigNumberValue.bind(this, isNegative, cb) : + this.#decodeBigNumberValue(isNegative, cb, chunk); + } + + #decodeBigNumberValue(isNegative, bigNumberCb, chunk) { + const bigNumber = bigNumberCb(chunk); + return typeof bigNumber === 'function' ? + this.#decodeBigNumberValue.bind(this, isNegative, bigNumber) : + isNegative ? -bigNumber : bigNumber; + } + + #decodeUnsingedBigNumber(bigNumber, chunk) { + let cursor = this.#cursor; + do { + const byte = chunk[cursor]; + if (byte === ASCII['\r']) { + this.#cursor = cursor + 2; // skip \r\n + return bigNumber; + } + bigNumber = bigNumber * 10n + BigInt(byte - ASCII['0']); + } while (++cursor < chunk.length); + + this.#cursor = cursor; + return this.#decodeUnsingedBigNumber.bind(this, bigNumber); + } + + #decodeDouble(type, chunk) { + if (type === String) { + return this.#decodeSimpleString(String, chunk); + } + + switch (chunk[this.#cursor]) { + case ASCII.n: + this.#cursor += 5; // skip nan\r\n + return NaN; + + case ASCII['+']: + return this.#maybeDecodeDoubleInteger(false, chunk); + + case ASCII['-']: + return this.#maybeDecodeDoubleInteger(true, chunk); + + default: + return this.#decodeDoubleInteger(false, 0, chunk); + } + } + + #maybeDecodeDoubleInteger(isNegative, chunk) { + return ++this.#cursor === chunk.length ? + this.#decodeDoubleInteger.bind(this, isNegative, 0) : + this.#decodeDoubleInteger(isNegative, 0, chunk); + } + + #decodeDoubleInteger(isNegative, integer, chunk) { + if (chunk[this.#cursor] === ASCII.i) { + this.#cursor += 5; // skip inf\r\n + return isNegative ? -Infinity : Infinity; + } + + return this.#continueDecodeDoubleInteger(isNegative, integer, chunk); + } + + #continueDecodeDoubleInteger(isNegative, integer, chunk) { + let cursor = this.#cursor; + do { + const byte = chunk[cursor]; + switch (byte) { + case ASCII['.']: + this.#cursor = cursor + 1; // skip . + return this.#cursor < chunk.length ? + this.#decodeDoubleDecimal(isNegative, 0, integer, chunk) : + this.#decodeDoubleDecimal.bind(this, isNegative, 0, integer); + + case ASCII.E: + case ASCII.e: + this.#cursor = cursor + 1; // skip E/e + const i = isNegative ? -integer : integer; + return this.#cursor < chunk.length ? + this.#decodeDoubleExponent(i, chunk) : + this.#decodeDoubleExponent.bind(this, i); + + case ASCII['\r']: + this.#cursor = cursor + 2; // skip \r\n + return isNegative ? -integer : integer; + + default: + integer = integer * 10 + byte - ASCII['0']; + } + } while (++cursor < chunk.length); + + this.#cursor = cursor; + return this.#continueDecodeDoubleInteger.bind(this, isNegative, integer); + } + + // Precalculated multipliers for decimal points to improve performance + // "... about 15 to 17 decimal places ..." + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number#:~:text=about%2015%20to%2017%20decimal%20places + static #DOUBLE_DECIMAL_MULTIPLIERS = [ + 1e-1, 1e-2, 1e-3, 1e-4, 1e-5, 1e-6, + 1e-7, 1e-8, 1e-9, 1e-10, 1e-11, 1e-12, + 1e-13, 1e-14, 1e-15, 1e-16, 1e-17 + ]; + + #decodeDoubleDecimal(isNegative, decimalIndex, double, chunk) { + let cursor = this.#cursor; + do { + const byte = chunk[cursor]; + switch (byte) { + case ASCII.E: + case ASCII.e: + this.#cursor = cursor + 1; // skip E/e + const d = isNegative ? -double : double; + return this.#cursor === chunk.length ? + this.#decodeDoubleExponent.bind(this, d) : + this.#decodeDoubleExponent(d, chunk); + + case ASCII['\r']: + this.#cursor = cursor + 2; // skip \r\n + return isNegative ? -double : double; + } + + if (decimalIndex < Decoder.#DOUBLE_DECIMAL_MULTIPLIERS.length) { + double += (byte - ASCII['0']) * Decoder.#DOUBLE_DECIMAL_MULTIPLIERS[decimalIndex++]; + } + } while (++cursor < chunk.length); + + this.#cursor = cursor; + return this.#decodeDoubleDecimal.bind(this, isNegative, decimalIndex, double); + } + + #decodeDoubleExponent(double, chunk) { + switch (chunk[this.#cursor]) { + case ASCII['+']: + return ++this.#cursor === chunk.length ? + this.#continueDecodeDoubleExponent.bind(this, false, double, 0) : + this.#continueDecodeDoubleExponent(false, double, 0, chunk); + + case ASCII['-']: + return ++this.#cursor === chunk.length ? + this.#continueDecodeDoubleExponent.bind(this, true, double, 0) : + this.#continueDecodeDoubleExponent(true, double, 0, chunk); + } + + return this.#continueDecodeDoubleExponent(false, double, 0, chunk); + } + + #continueDecodeDoubleExponent(isNegative, double, exponent, chunk) { + let cursor = this.#cursor; + do { + const byte = chunk[cursor]; + if (byte === ASCII['\r']) { + this.#cursor = cursor + 2; // skip \r\n + return double * 10 ** (isNegative ? -exponent : exponent); + } + + exponent = exponent * 10 + byte - ASCII['0']; + } while (++cursor < chunk.length); + + this.#cursor = cursor; + return this.#continueDecodeDoubleExponent.bind(this, isNegative, double, exponent); + } + + #findCRLF(chunk, cursor) { + while (chunk[cursor] !== ASCII['\r']) { + if (++cursor === chunk.length) { + this.#cursor = chunk.length; + return -1; + } + } + + this.#cursor = cursor + 2; // skip \r\n + return cursor; + } + + #decodeSimpleString(type, chunk) { + const start = this.#cursor, + crlfIndex = this.#findCRLF(chunk, start); + if (crlfIndex === -1) { + return this.#continueDecodeSimpleString.bind( + this, + [chunk.subarray(start)], + type + ); + } + + const slice = chunk.subarray(start, crlfIndex); + return type === Buffer ? + slice : + slice.toString(); + } + + #continueDecodeSimpleString(chunks, type, chunk) { + const start = this.#cursor, + crlfIndex = this.#findCRLF(chunk, start); + if (crlfIndex === -1) { + chunks.push(chunk.subarray(start)); + return this.#continueDecodeSimpleString.bind(this, chunks, type); + } + + chunks.push(chunk.subarray(start, crlfIndex)); + return type === Buffer ? + Buffer.concat(chunks) : + chunks.join(''); + } + + #decodeBlobString(type, chunk) { + // RESP 2 bulk string null + // https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md#resp-bulk-strings + if (chunk[this.#cursor] === ASCII['-']) { + this.#cursor += 4; // skip -1\r\n + return null; + } + + const length = this.#decodeUnsingedNumber(0, chunk); + if (typeof length === 'function') { + return this.#continueDecodeBlobStringLength.bind(this, length, type); + } else if (this.#cursor >= chunk.length) { + return this.#decodeBlobStringWithLength.bind(this, length, type); + } + + return this.#decodeBlobStringWithLength(length, type, chunk); + } + + #continueDecodeBlobStringLength(lengthCb, type, chunk) { + const length = lengthCb(chunk); + if (typeof length === 'function') { + return this.#continueDecodeBlobStringLength.bind(this, length, type); + } else if (this.#cursor >= chunk.length) { + return this.#decodeBlobStringWithLength.bind(this, length, type); + } + + return this.#decodeBlobStringWithLength(length, type, chunk); + } + + #decodeStringWithLength(length, skip, type, chunk) { + const end = this.#cursor + length; + if (end >= chunk.length) { + const slice = chunk.subarray(this.#cursor); + this.#cursor = chunk.length; + return this.#continueDecodeStringWithLength.bind( + this, + length - slice.length, + [slice], + skip, + type + ); + } + + const slice = chunk.subarray(this.#cursor, end); + this.#cursor = end + skip; + return type === Buffer ? + slice : + slice.toString(); + } + + #continueDecodeStringWithLength(length, chunks, skip, type, chunk) { + const end = this.#cursor + length; + if (end >= chunk.length) { + const slice = chunk.subarray(this.#cursor); + chunks.push(slice); + this.#cursor = chunk.length; + return this.#continueDecodeStringWithLength.bind( + this, + length - slice.length, + chunks, + skip, + type + ); + } + + chunks.push(chunk.subarray(this.#cursor, end)); + this.#cursor = end + skip; + return type === Buffer ? + Buffer.concat(chunks) : + chunks.join(''); + } + + #decodeBlobStringWithLength(length, type, chunk) { + return this.#decodeStringWithLength(length, 2, type, chunk); + } + + #decodeVerbatimString(type, chunk) { + return this.#continueDecodeVerbatimStringLength( + this.#decodeUnsingedNumber.bind(this, 0), + type, + chunk + ); + } + + #continueDecodeVerbatimStringLength(lengthCb, type, chunk) { + const length = lengthCb(chunk); + return typeof length === 'function' ? + this.#continueDecodeVerbatimStringLength.bind(this, length, type) : + this.#decodeVerbatimStringWithLength(length, type, chunk); + } + + #decodeVerbatimStringWithLength(length, type, chunk) { + const stringLength = length - 4; // skip : + if (type === VerbatimString) { + return this.#decodeVerbatimStringFormat(stringLength, chunk); + } + + this.#cursor += 4; // skip : + return this.#cursor >= chunk.length ? + this.#decodeBlobStringWithLength.bind(this, stringLength, type) : + this.#decodeBlobStringWithLength(stringLength, type, chunk); + } + + #decodeVerbatimStringFormat(stringLength, chunk) { + const formatCb = this.#decodeStringWithLength.bind(this, 3, 1, String); + return this.#cursor >= chunk.length ? + this.#continueDecodeVerbatimStringFormat.bind(this, stringLength, formatCb) : + this.#continueDecodeVerbatimStringFormat(stringLength, formatCb, chunk); + } + + #continueDecodeVerbatimStringFormat(stringLength, formatCb, chunk) { + const format = formatCb(chunk); + return typeof format === 'function' ? + this.#continueDecodeVerbatimStringFormat.bind(this, stringLength, format) : + this.#decodeVerbatimStringWithFormat(stringLength, format, chunk); + } + + #decodeVerbatimStringWithFormat(stringLength, format, chunk) { + return this.#continueDecodeVerbatimStringWithFormat( + format, + this.#decodeBlobStringWithLength.bind(this, stringLength, String), + chunk + ); + } + + #continueDecodeVerbatimStringWithFormat(format, stringCb, chunk) { + const string = stringCb(chunk); + return typeof string === 'function' ? + this.#continueDecodeVerbatimStringWithFormat.bind(this, format, string) : + new VerbatimString(format, string); + } + + #decodeSimpleError(chunk) { + const string = this.#decodeSimpleString(String, chunk); + return typeof string === 'function' ? + this.#continueDecodeSimpleError.bind(this, string) : + new SimpleError(string); + } + + #continueDecodeSimpleError(stringCb, chunk) { + const string = stringCb(chunk); + return typeof string === 'function' ? + this.#continueDecodeSimpleError.bind(this, string) : + new SimpleError(string); + } + + #decodeBlobError(chunk) { + const string = this.#decodeBlobString(String, chunk); + return typeof string === 'function' ? + this.#continueDecodeBlobError.bind(this, string) : + new BlobError(string); + } + + #continueDecodeBlobError(stringCb, chunk) { + const string = stringCb(chunk); + return typeof string === 'function' ? + this.#continueDecodeBlobError.bind(this, string) : + new BlobError(string); + } + + #decodeNestedType(typeMapping, chunk) { + const type = chunk[this.#cursor]; + return ++this.#cursor === chunk.length ? + this.#decodeNestedTypeValue.bind(this, type, typeMapping) : + this.#decodeNestedTypeValue(type, typeMapping, chunk); + } + + #decodeNestedTypeValue(type, typeMapping, chunk) { + switch (type) { + case RESP_TYPES.NULL: + return this.#decodeNull(); + + case RESP_TYPES.BOOLEAN: + return this.#decodeBoolean(chunk); + + case RESP_TYPES.NUMBER: + return this.#decodeNumber(typeMapping[RESP_TYPES.NUMBER], chunk); + + case RESP_TYPES.BIG_NUMBER: + return this.#decodeBigNumber(typeMapping[RESP_TYPES.BIG_NUMBER], chunk); + + case RESP_TYPES.DOUBLE: + return this.#decodeDouble(typeMapping[RESP_TYPES.DOUBLE], chunk); + + case RESP_TYPES.SIMPLE_STRING: + return this.#decodeSimpleString(typeMapping[RESP_TYPES.SIMPLE_STRING], chunk); + + case RESP_TYPES.BLOB_STRING: + return this.#decodeBlobString(typeMapping[RESP_TYPES.BLOB_STRING], chunk); + + case RESP_TYPES.VERBATIM_STRING: + return this.#decodeVerbatimString(typeMapping[RESP_TYPES.VERBATIM_STRING], chunk); + + case RESP_TYPES.SIMPLE_ERROR: + return this.#decodeSimpleError(chunk); + + case RESP_TYPES.BLOB_ERROR: + return this.#decodeBlobError(chunk); + + case RESP_TYPES.ARRAY: + return this.#decodeArray(typeMapping, chunk); + + case RESP_TYPES.SET: + return this.#decodeSet(typeMapping, chunk); + + case RESP_TYPES.MAP: + return this.#decodeMap(typeMapping, chunk); + + default: + throw new Error(`Unknown RESP type ${type} "${String.fromCharCode(type)}"`); + } + } + + #decodeArray(typeMapping, chunk) { + // RESP 2 null + // https://github.com/redis/redis-specifications/blob/master/protocol/RESP2.md#resp-arrays + if (chunk[this.#cursor] === ASCII['-']) { + this.#cursor += 4; // skip -1\r\n + return null; + } + + return this.#decodeArrayWithLength( + this.#decodeUnsingedNumber(0, chunk), + typeMapping, + chunk + ); + } + + #decodeArrayWithLength(length, typeMapping, chunk) { + return typeof length === 'function' ? + this.#continueDecodeArrayLength.bind(this, length, typeMapping) : + this.#decodeArrayItems( + new Array(length), + 0, + typeMapping, + chunk + ); + } + + #continueDecodeArrayLength(lengthCb, typeMapping, chunk) { + return this.#decodeArrayWithLength( + lengthCb(chunk), + typeMapping, + chunk + ); + } + + #decodeArrayItems(array, filled, typeMapping, chunk) { + for (let i = filled; i < array.length; i++) { + if (this.#cursor >= chunk.length) { + return this.#decodeArrayItems.bind( + this, + array, + i, + typeMapping + ); + } + + const item = this.#decodeNestedType(typeMapping, chunk); + if (typeof item === 'function') { + return this.#continueDecodeArrayItems.bind( + this, + array, + i, + item, + typeMapping + ); + } + + array[i] = item; + } + + return array; + } + + #continueDecodeArrayItems(array, filled, itemCb, typeMapping, chunk) { + const item = itemCb(chunk); + if (typeof item === 'function') { + return this.#continueDecodeArrayItems.bind( + this, + array, + filled, + item, + typeMapping + ); + } + + array[filled++] = item; + + return this.#decodeArrayItems(array, filled, typeMapping, chunk); + } + + #decodeSet(typeMapping, chunk) { + const length = this.#decodeUnsingedNumber(0, chunk); + if (typeof length === 'function') { + return this.#continueDecodeSetLength.bind(this, length, typeMapping); + } + + return this.#decodeSetItems( + length, + typeMapping, + chunk + ); + } + + #continueDecodeSetLength(lengthCb, typeMapping, chunk) { + const length = lengthCb(chunk); + return typeof length === 'function' ? + this.#continueDecodeSetLength.bind(this, length, typeMapping) : + this.#decodeSetItems(length, typeMapping, chunk); + } + + #decodeSetItems(length, typeMapping, chunk) { + return typeMapping[RESP_TYPES.SET] === Set ? + this.#decodeSetAsSet( + new Set(), + length, + typeMapping, + chunk + ) : + this.#decodeArrayItems( + new Array(length), + 0, + typeMapping, + chunk + ); + } + + #decodeSetAsSet(set, remaining, typeMapping, chunk) { + // using `remaining` instead of `length` & `set.size` to make it work even if the set contains duplicates + while (remaining > 0) { + if (this.#cursor >= chunk.length) { + return this.#decodeSetAsSet.bind( + this, + set, + remaining, + typeMapping + ); + } + + const item = this.#decodeNestedType(typeMapping, chunk); + if (typeof item === 'function') { + return this.#continueDecodeSetAsSet.bind( + this, + set, + remaining, + item, + typeMapping + ); + } + + set.add(item); + --remaining; + } + + return set; + } + + #continueDecodeSetAsSet(set, remaining, itemCb, typeMapping, chunk) { + const item = itemCb(chunk); + if (typeof item === 'function') { + return this.#continueDecodeSetAsSet.bind( + this, + set, + remaining, + item, + typeMapping + ); + } + + set.add(item); + + return this.#decodeSetAsSet(set, remaining - 1, typeMapping, chunk); + } + + #decodeMap(typeMapping, chunk) { + const length = this.#decodeUnsingedNumber(0, chunk); + if (typeof length === 'function') { + return this.#continueDecodeMapLength.bind(this, length, typeMapping); + } + + return this.#decodeMapItems( + length, + typeMapping, + chunk + ); + } + + #continueDecodeMapLength(lengthCb, typeMapping, chunk) { + const length = lengthCb(chunk); + return typeof length === 'function' ? + this.#continueDecodeMapLength.bind(this, length, typeMapping) : + this.#decodeMapItems(length, typeMapping, chunk); + } + + #decodeMapItems(length, typeMapping, chunk) { + switch (typeMapping[RESP_TYPES.MAP]) { + case Map: + return this.#decodeMapAsMap( + new Map(), + length, + typeMapping, + chunk + ); + + case Array: + return this.#decodeArrayItems( + new Array(length * 2), + 0, + typeMapping, + chunk + ); + + default: + return this.#decodeMapAsObject( + Object.create(null), + length, + typeMapping, + chunk + ); + } + } + + #decodeMapAsMap(map, remaining, typeMapping, chunk) { + // using `remaining` instead of `length` & `map.size` to make it work even if the map contains duplicate keys + while (remaining > 0) { + if (this.#cursor >= chunk.length) { + return this.#decodeMapAsMap.bind( + this, + map, + remaining, + typeMapping + ); + } + + const key = this.#decodeMapKey(typeMapping, chunk); + if (typeof key === 'function') { + return this.#continueDecodeMapKey.bind( + this, + map, + remaining, + key, + typeMapping + ); + } + + if (this.#cursor >= chunk.length) { + return this.#continueDecodeMapValue.bind( + this, + map, + remaining, + key, + this.#decodeNestedType.bind(this, typeMapping), + typeMapping + ); + } + + const value = this.#decodeNestedType(typeMapping, chunk); + if (typeof value === 'function') { + return this.#continueDecodeMapValue.bind( + this, + map, + remaining, + key, + value, + typeMapping + ); + } + + map.set(key, value); + --remaining; + } + + return map; + } + + #decodeMapKey(typeMapping, chunk) { + const type = chunk[this.#cursor]; + return ++this.#cursor === chunk.length ? + this.#decodeMapKeyValue.bind(this, type, typeMapping) : + this.#decodeMapKeyValue(type, typeMapping, chunk); + } + + #decodeMapKeyValue(type, typeMapping, chunk) { + switch (type) { + // decode simple string map key as string (and not as buffer) + case RESP_TYPES.SIMPLE_STRING: + return this.#decodeSimpleString(String, chunk); + + // decode blob string map key as string (and not as buffer) + case RESP_TYPES.BLOB_STRING: + return this.#decodeBlobString(String, chunk); + + default: + return this.#decodeNestedTypeValue(type, typeMapping, chunk); + } + } + + #continueDecodeMapKey(map, remaining, keyCb, typeMapping, chunk) { + const key = keyCb(chunk); + if (typeof key === 'function') { + return this.#continueDecodeMapKey.bind( + this, + map, + remaining, + key, + typeMapping + ); + } + + if (this.#cursor >= chunk.length) { + return this.#continueDecodeMapValue.bind( + this, + map, + remaining, + key, + this.#decodeNestedType.bind(this, typeMapping), + typeMapping + ); + } + + const value = this.#decodeNestedType(typeMapping, chunk); + if (typeof value === 'function') { + return this.#continueDecodeMapValue.bind( + this, + map, + remaining, + key, + value, + typeMapping + ); + } + + map.set(key, value); + return this.#decodeMapAsMap(map, remaining - 1, typeMapping, chunk); + } + + #continueDecodeMapValue(map, remaining, key, valueCb, typeMapping, chunk) { + const value = valueCb(chunk); + if (typeof value === 'function') { + return this.#continueDecodeMapValue.bind( + this, + map, + remaining, + key, + value, + typeMapping + ); + } + + map.set(key, value); + + return this.#decodeMapAsMap(map, remaining - 1, typeMapping, chunk); + } + + #decodeMapAsObject(object, remaining, typeMapping, chunk) { + while (remaining > 0) { + if (this.#cursor >= chunk.length) { + return this.#decodeMapAsObject.bind( + this, + object, + remaining, + typeMapping + ); + } + + const key = this.#decodeMapKey(typeMapping, chunk); + if (typeof key === 'function') { + return this.#continueDecodeMapAsObjectKey.bind( + this, + object, + remaining, + key, + typeMapping + ); + } + + if (this.#cursor >= chunk.length) { + return this.#continueDecodeMapAsObjectValue.bind( + this, + object, + remaining, + key, + this.#decodeNestedType.bind(this, typeMapping), + typeMapping + ); + } + + const value = this.#decodeNestedType(typeMapping, chunk); + if (typeof value === 'function') { + return this.#continueDecodeMapAsObjectValue.bind( + this, + object, + remaining, + key, + value, + typeMapping + ); + } + + object[key] = value; + --remaining; + } + + return object; + } + + #continueDecodeMapAsObjectKey(object, remaining, keyCb, typeMapping, chunk) { + const key = keyCb(chunk); + if (typeof key === 'function') { + return this.#continueDecodeMapAsObjectKey.bind( + this, + object, + remaining, + key, + typeMapping + ); + } + + if (this.#cursor >= chunk.length) { + return this.#continueDecodeMapAsObjectValue.bind( + this, + object, + remaining, + key, + this.#decodeNestedType.bind(this, typeMapping), + typeMapping + ); + } + + const value = this.#decodeNestedType(typeMapping, chunk); + if (typeof value === 'function') { + return this.#continueDecodeMapAsObjectValue.bind( + this, + object, + remaining, + key, + value, + typeMapping + ); + } + + object[key] = value; + + return this.#decodeMapAsObject(object, remaining - 1, typeMapping, chunk); + } + + #continueDecodeMapAsObjectValue(object, remaining, key, valueCb, typeMapping, chunk) { + const value = valueCb(chunk); + if (typeof value === 'function') { + return this.#continueDecodeMapAsObjectValue.bind( + this, + object, + remaining, + key, + value, + typeMapping + ); + } + + object[key] = value; + + return this.#decodeMapAsObject(object, remaining - 1, typeMapping, chunk); + } +} diff --git a/packages/client/lib/RESP/encoder.spec.ts b/packages/client/lib/RESP/encoder.spec.ts new file mode 100644 index 00000000000..2cbdc7d0b24 --- /dev/null +++ b/packages/client/lib/RESP/encoder.spec.ts @@ -0,0 +1,33 @@ +import { strict as assert } from 'node:assert'; +import { describe } from 'mocha'; +import encodeCommand from './encoder'; + +describe('RESP Encoder', () => { + it('1 byte', () => { + assert.deepEqual( + encodeCommand(['a', 'z']), + ['*2\r\n$1\r\na\r\n$1\r\nz\r\n'] + ); + }); + + it('2 bytes', () => { + assert.deepEqual( + encodeCommand(['א', 'ת']), + ['*2\r\n$2\r\nא\r\n$2\r\nת\r\n'] + ); + }); + + it('4 bytes', () => { + assert.deepEqual( + [...encodeCommand(['🐣', '🐤'])], + ['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n'] + ); + }); + + it('buffer', () => { + assert.deepEqual( + encodeCommand([Buffer.from('string')]), + ['*1\r\n$6\r\n', Buffer.from('string'), '\r\n'] + ); + }); +}); diff --git a/packages/client/lib/RESP/encoder.ts b/packages/client/lib/RESP/encoder.ts new file mode 100644 index 00000000000..af857711dc3 --- /dev/null +++ b/packages/client/lib/RESP/encoder.ts @@ -0,0 +1,28 @@ +import { RedisArgument } from './types'; + +const CRLF = '\r\n'; + +export default function encodeCommand(args: Array): Array { + const toWrite: Array = []; + + let strings = '*' + args.length + CRLF; + + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + if (typeof arg === 'string') { + strings += '$' + Buffer.byteLength(arg) + CRLF + arg + CRLF; + } else if (arg instanceof Buffer) { + toWrite.push( + strings + '$' + arg.length.toString() + CRLF, + arg + ); + strings = CRLF; + } else { + throw new TypeError(`"arguments[${i}]" must be of type "string | Buffer", got ${typeof arg} instead.`); + } + } + + toWrite.push(strings); + + return toWrite; +} diff --git a/packages/client/lib/RESP/types.ts b/packages/client/lib/RESP/types.ts new file mode 100644 index 00000000000..46fcd7ac8c1 --- /dev/null +++ b/packages/client/lib/RESP/types.ts @@ -0,0 +1,398 @@ +import { BlobError, SimpleError } from '../errors'; +import { RedisScriptConfig, SHA1 } from '../lua-script'; +import { RESP_TYPES } from './decoder'; +import { VerbatimString } from './verbatim-string'; + +export type RESP_TYPES = typeof RESP_TYPES; + +export type RespTypes = RESP_TYPES[keyof RESP_TYPES]; + +// using interface(s) to allow circular references +// type X = BlobStringReply | ArrayReply; + +export interface RespType< + RESP_TYPE extends RespTypes, + DEFAULT, + TYPES = never, + TYPE_MAPPING = DEFAULT | TYPES +> { + RESP_TYPE: RESP_TYPE; + DEFAULT: DEFAULT; + TYPES: TYPES; + TYPE_MAPPING: MappedType; +} + +export interface NullReply extends RespType< + RESP_TYPES['NULL'], + null +> {} + +export interface BooleanReply< + T extends boolean = boolean +> extends RespType< + RESP_TYPES['BOOLEAN'], + T +> {} + +export interface NumberReply< + T extends number = number +> extends RespType< + RESP_TYPES['NUMBER'], + T, + `${T}`, + number | string +> {} + +export interface BigNumberReply< + T extends bigint = bigint +> extends RespType< + RESP_TYPES['BIG_NUMBER'], + T, + number | `${T}`, + bigint | number | string +> {} + +export interface DoubleReply< + T extends number = number +> extends RespType< + RESP_TYPES['DOUBLE'], + T, + `${T}`, + number | string +> {} + +export interface SimpleStringReply< + T extends string = string +> extends RespType< + RESP_TYPES['SIMPLE_STRING'], + T, + Buffer, + string | Buffer +> {} + +export interface BlobStringReply< + T extends string = string +> extends RespType< + RESP_TYPES['BLOB_STRING'], + T, + Buffer, + string | Buffer +> { + toString(): string +} + +export interface VerbatimStringReply< + T extends string = string +> extends RespType< + RESP_TYPES['VERBATIM_STRING'], + T, + Buffer | VerbatimString, + string | Buffer | VerbatimString +> {} + +export interface SimpleErrorReply extends RespType< + RESP_TYPES['SIMPLE_ERROR'], + SimpleError, + Buffer +> {} + +export interface BlobErrorReply extends RespType< + RESP_TYPES['BLOB_ERROR'], + BlobError, + Buffer +> {} + +export interface ArrayReply extends RespType< + RESP_TYPES['ARRAY'], + Array, + never, + Array +> {} + +export interface TuplesReply]> extends RespType< + RESP_TYPES['ARRAY'], + T, + never, + Array +> {} + +export interface SetReply extends RespType< + RESP_TYPES['SET'], + Array, + Set, + Array | Set +> {} + +export interface MapReply extends RespType< + RESP_TYPES['MAP'], + { [key: string]: V }, + Map | Array, + Map | Array +> {} + +type MapKeyValue = [key: BlobStringReply | SimpleStringReply, value: unknown]; + +type MapTuples = Array; + +type ExtractMapKey = ( + T extends BlobStringReply ? S : + T extends SimpleStringReply ? S : + never +); + +export interface TuplesToMapReply extends RespType< + RESP_TYPES['MAP'], + { + [P in T[number] as ExtractMapKey]: P[1]; + }, + Map, T[number][1]> | FlattenTuples +> {} + +type FlattenTuples = ( + T extends [] ? [] : + T extends [MapKeyValue] ? T[0] : + T extends [MapKeyValue, ...infer R] ? [ + ...T[0], + ...FlattenTuples + ] : + never +); + +export type ReplyUnion = ( + NullReply | + BooleanReply | + NumberReply | + BigNumberReply | + DoubleReply | + SimpleStringReply | + BlobStringReply | + VerbatimStringReply | + SimpleErrorReply | + BlobErrorReply | + ArrayReply | + SetReply | + MapReply +); + +export type MappedType = ((...args: any) => T) | (new (...args: any) => T); + +type InferTypeMapping = T extends RespType ? FLAG_TYPES : never; + +export type TypeMapping = { + [P in RespTypes]?: MappedType>>>; +}; + +type MapKey< + T, + TYPE_MAPPING extends TypeMapping +> = ReplyWithTypeMapping; + +export type UnwrapReply> = REPLY['DEFAULT' | 'TYPES']; + +export type ReplyWithTypeMapping< + REPLY, + TYPE_MAPPING extends TypeMapping +> = ( + // if REPLY is a type, extract the coresponding type from TYPE_MAPPING or use the default type + REPLY extends RespType ? + TYPE_MAPPING[RESP_TYPE] extends MappedType ? + ReplyWithTypeMapping, TYPE_MAPPING> : + ReplyWithTypeMapping + : ( + // if REPLY is a known generic type, convert its generic arguments + // TODO: tuples? + REPLY extends Array ? Array> : + REPLY extends Set ? Set> : + REPLY extends Map ? Map, ReplyWithTypeMapping> : + // `Date | Buffer | Error` are supersets of `Record`, so they need to be checked first + REPLY extends Date | Buffer | Error ? REPLY : + REPLY extends Record ? { + [P in keyof REPLY]: ReplyWithTypeMapping; + } : + // otherwise, just return the REPLY as is + REPLY + ) +); + +export type TransformReply = (this: void, reply: any, preserve?: any, typeMapping?: TypeMapping) => any; // TODO; + +export type RedisArgument = string | Buffer; + +export type CommandArguments = Array & { preserve?: unknown }; + +// export const REQUEST_POLICIES = { +// /** +// * TODO +// */ +// ALL_NODES: 'all_nodes', +// /** +// * TODO +// */ +// ALL_SHARDS: 'all_shards', +// /** +// * TODO +// */ +// SPECIAL: 'special' +// } as const; + +// export type REQUEST_POLICIES = typeof REQUEST_POLICIES; + +// export type RequestPolicies = REQUEST_POLICIES[keyof REQUEST_POLICIES]; + +// export const RESPONSE_POLICIES = { +// /** +// * TODO +// */ +// ONE_SUCCEEDED: 'one_succeeded', +// /** +// * TODO +// */ +// ALL_SUCCEEDED: 'all_succeeded', +// /** +// * TODO +// */ +// LOGICAL_AND: 'agg_logical_and', +// /** +// * TODO +// */ +// SPECIAL: 'special' +// } as const; + +// export type RESPONSE_POLICIES = typeof RESPONSE_POLICIES; + +// export type ResponsePolicies = RESPONSE_POLICIES[keyof RESPONSE_POLICIES]; + +// export type CommandPolicies = { +// request?: RequestPolicies | null; +// response?: ResponsePolicies | null; +// }; + +export type Command = { + FIRST_KEY_INDEX?: number | ((this: void, ...args: Array) => RedisArgument | undefined); + IS_READ_ONLY?: boolean; + /** + * @internal + * TODO: remove once `POLICIES` is implemented + */ + IS_FORWARD_COMMAND?: boolean; + // POLICIES?: CommandPolicies; + transformArguments(this: void, ...args: Array): CommandArguments; + TRANSFORM_LEGACY_REPLY?: boolean; + transformReply: TransformReply | Record; + unstableResp3?: boolean; +}; + +export type RedisCommands = Record; + +export type RedisModules = Record; + +export interface RedisFunction extends Command { + NUMBER_OF_KEYS?: number; +} + +export type RedisFunctions = Record>; + +export type RedisScript = RedisScriptConfig & SHA1; + +export type RedisScripts = Record; + +// TODO: move to Commander? +export interface CommanderConfig< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions +> { + modules?: M; + functions?: F; + scripts?: S; + /** + * TODO + */ + RESP?: RESP; + /** + * TODO + */ + unstableResp3?: boolean; +} + +type Resp2Array = ( + T extends [] ? [] : + T extends [infer ITEM] ? [Resp2Reply] : + T extends [infer ITEM, ...infer REST] ? [ + Resp2Reply, + ...Resp2Array + ] : + T extends Array ? Array> : + never +); + +export type Resp2Reply = ( + RESP3REPLY extends RespType ? + // TODO: RESP3 only scalar types + RESP_TYPE extends RESP_TYPES['DOUBLE'] ? BlobStringReply : + RESP_TYPE extends RESP_TYPES['ARRAY'] | RESP_TYPES['SET'] ? RespType< + RESP_TYPE, + Resp2Array + > : + RESP_TYPE extends RESP_TYPES['MAP'] ? RespType< + RESP_TYPES['ARRAY'], + Resp2Array>> + > : + RESP3REPLY : + RESP3REPLY +); + +export type RespVersions = 2 | 3; + +export type CommandReply< + COMMAND extends Command, + RESP extends RespVersions +> = ( + // if transformReply is a function, use its return type + COMMAND['transformReply'] extends (...args: any) => infer T ? T : + // if transformReply[RESP] is a function, use its return type + COMMAND['transformReply'] extends Record infer T> ? T : + // otherwise use the generic reply type + ReplyUnion +); + +export type CommandSignature< + COMMAND extends Command, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = (...args: Parameters) => Promise, TYPE_MAPPING>>; + +// export type CommandWithPoliciesSignature< +// COMMAND extends Command, +// RESP extends RespVersions, +// TYPE_MAPPING extends TypeMapping, +// POLICIES extends CommandPolicies +// > = (...args: Parameters) => Promise< +// ReplyWithPolicy< +// ReplyWithTypeMapping, TYPE_MAPPING>, +// MergePolicies +// > +// >; + +// export type MergePolicies< +// COMMAND extends Command, +// POLICIES extends CommandPolicies +// > = Omit & POLICIES; + +// type ReplyWithPolicy< +// REPLY, +// POLICIES extends CommandPolicies, +// > = ( +// POLICIES['request'] extends REQUEST_POLICIES['SPECIAL'] ? never : +// POLICIES['request'] extends null | undefined ? REPLY : +// unknown extends POLICIES['request'] ? REPLY : +// POLICIES['response'] extends RESPONSE_POLICIES['SPECIAL'] ? never : +// POLICIES['response'] extends RESPONSE_POLICIES['ALL_SUCCEEDED' | 'ONE_SUCCEEDED' | 'LOGICAL_AND'] ? REPLY : +// // otherwise, return array of replies +// Array +// ); diff --git a/packages/client/lib/RESP/verbatim-string.ts b/packages/client/lib/RESP/verbatim-string.ts new file mode 100644 index 00000000000..92ff4fe3fb1 --- /dev/null +++ b/packages/client/lib/RESP/verbatim-string.ts @@ -0,0 +1,8 @@ +export class VerbatimString extends String { + constructor( + public format: string, + value: string + ) { + super(value); + } +} diff --git a/packages/client/lib/client/RESP2/composers/buffer.spec.ts b/packages/client/lib/client/RESP2/composers/buffer.spec.ts deleted file mode 100644 index f57c369fecb..00000000000 --- a/packages/client/lib/client/RESP2/composers/buffer.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { strict as assert } from 'assert'; -import BufferComposer from './buffer'; - -describe('Buffer Composer', () => { - const composer = new BufferComposer(); - - it('should compose two buffers', () => { - composer.write(Buffer.from([0])); - assert.deepEqual( - composer.end(Buffer.from([1])), - Buffer.from([0, 1]) - ); - }); -}); diff --git a/packages/client/lib/client/RESP2/composers/buffer.ts b/packages/client/lib/client/RESP2/composers/buffer.ts deleted file mode 100644 index 4affb4283e0..00000000000 --- a/packages/client/lib/client/RESP2/composers/buffer.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { Composer } from './interface'; - -export default class BufferComposer implements Composer { - private chunks: Array = []; - - write(buffer: Buffer): void { - this.chunks.push(buffer); - } - - end(buffer: Buffer): Buffer { - this.write(buffer); - return Buffer.concat(this.chunks.splice(0)); - } - - reset() { - this.chunks = []; - } -} diff --git a/packages/client/lib/client/RESP2/composers/interface.ts b/packages/client/lib/client/RESP2/composers/interface.ts deleted file mode 100644 index 0fc8f031414..00000000000 --- a/packages/client/lib/client/RESP2/composers/interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface Composer { - write(buffer: Buffer): void; - - end(buffer: Buffer): T; - - reset(): void; -} diff --git a/packages/client/lib/client/RESP2/composers/string.spec.ts b/packages/client/lib/client/RESP2/composers/string.spec.ts deleted file mode 100644 index 9dd26aae021..00000000000 --- a/packages/client/lib/client/RESP2/composers/string.spec.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { strict as assert } from 'assert'; -import StringComposer from './string'; - -describe('String Composer', () => { - const composer = new StringComposer(); - - it('should compose two strings', () => { - composer.write(Buffer.from([0])); - assert.deepEqual( - composer.end(Buffer.from([1])), - Buffer.from([0, 1]).toString() - ); - }); -}); diff --git a/packages/client/lib/client/RESP2/composers/string.ts b/packages/client/lib/client/RESP2/composers/string.ts deleted file mode 100644 index 0cd8f00e95c..00000000000 --- a/packages/client/lib/client/RESP2/composers/string.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { StringDecoder } from 'string_decoder'; -import { Composer } from './interface'; - -export default class StringComposer implements Composer { - private decoder = new StringDecoder(); - - private string = ''; - - write(buffer: Buffer): void { - this.string += this.decoder.write(buffer); - } - - end(buffer: Buffer): string { - const string = this.string + this.decoder.end(buffer); - this.string = ''; - return string; - } - - reset() { - this.string = ''; - } -} diff --git a/packages/client/lib/client/RESP2/decoder.spec.ts b/packages/client/lib/client/RESP2/decoder.spec.ts deleted file mode 100644 index dcce9f60115..00000000000 --- a/packages/client/lib/client/RESP2/decoder.spec.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { strict as assert } from 'assert'; -import { SinonSpy, spy } from 'sinon'; -import RESP2Decoder from './decoder'; -import { ErrorReply } from '../../errors'; - -interface DecoderAndSpies { - decoder: RESP2Decoder; - returnStringsAsBuffersSpy: SinonSpy; - onReplySpy: SinonSpy; -} - -function createDecoderAndSpies(returnStringsAsBuffers: boolean): DecoderAndSpies { - const returnStringsAsBuffersSpy = spy(() => returnStringsAsBuffers), - onReplySpy = spy(); - - return { - decoder: new RESP2Decoder({ - returnStringsAsBuffers: returnStringsAsBuffersSpy, - onReply: onReplySpy - }), - returnStringsAsBuffersSpy, - onReplySpy - }; -} - -function writeChunks(stream: RESP2Decoder, buffer: Buffer) { - let i = 0; - while (i < buffer.length) { - stream.write(buffer.slice(i, ++i)); - } -} - -type Replies = Array>; - -interface TestsOptions { - toWrite: Buffer; - returnStringsAsBuffers: boolean; - replies: Replies; -} - -function generateTests({ - toWrite, - returnStringsAsBuffers, - replies -}: TestsOptions): void { - it('single chunk', () => { - const { decoder, returnStringsAsBuffersSpy, onReplySpy } = - createDecoderAndSpies(returnStringsAsBuffers); - decoder.write(toWrite); - assert.equal(returnStringsAsBuffersSpy.callCount, replies.length); - testReplies(onReplySpy, replies); - }); - - it('multiple chunks', () => { - const { decoder, returnStringsAsBuffersSpy, onReplySpy } = - createDecoderAndSpies(returnStringsAsBuffers); - writeChunks(decoder, toWrite); - assert.equal(returnStringsAsBuffersSpy.callCount, replies.length); - testReplies(onReplySpy, replies); - }); -} - -function testReplies(spy: SinonSpy, replies: Replies): void { - if (!replies) { - assert.equal(spy.callCount, 0); - return; - } - - assert.equal(spy.callCount, replies.length); - for (const [i, reply] of replies.entries()) { - assert.deepEqual( - spy.getCall(i).args, - reply - ); - } -} - -describe('RESP2Parser', () => { - describe('Simple String', () => { - describe('as strings', () => { - generateTests({ - toWrite: Buffer.from('+OK\r\n'), - returnStringsAsBuffers: false, - replies: [['OK']] - }); - }); - - describe('as buffers', () => { - generateTests({ - toWrite: Buffer.from('+OK\r\n'), - returnStringsAsBuffers: true, - replies: [[Buffer.from('OK')]] - }); - }); - }); - - describe('Error', () => { - generateTests({ - toWrite: Buffer.from('-ERR\r\n'), - returnStringsAsBuffers: false, - replies: [[new ErrorReply('ERR')]] - }); - }); - - describe('Integer', () => { - describe('-1', () => { - generateTests({ - toWrite: Buffer.from(':-1\r\n'), - returnStringsAsBuffers: false, - replies: [[-1]] - }); - }); - - describe('0', () => { - generateTests({ - toWrite: Buffer.from(':0\r\n'), - returnStringsAsBuffers: false, - replies: [[0]] - }); - }); - }); - - describe('Bulk String', () => { - describe('null', () => { - generateTests({ - toWrite: Buffer.from('$-1\r\n'), - returnStringsAsBuffers: false, - replies: [[null]] - }); - }); - - describe('as strings', () => { - generateTests({ - toWrite: Buffer.from('$2\r\naa\r\n'), - returnStringsAsBuffers: false, - replies: [['aa']] - }); - }); - - describe('as buffers', () => { - generateTests({ - toWrite: Buffer.from('$2\r\naa\r\n'), - returnStringsAsBuffers: true, - replies: [[Buffer.from('aa')]] - }); - }); - }); - - describe('Array', () => { - describe('null', () => { - generateTests({ - toWrite: Buffer.from('*-1\r\n'), - returnStringsAsBuffers: false, - replies: [[null]] - }); - }); - - const arrayBuffer = Buffer.from( - '*5\r\n' + - '+OK\r\n' + - '-ERR\r\n' + - ':0\r\n' + - '$1\r\na\r\n' + - '*0\r\n' - ); - - describe('as strings', () => { - generateTests({ - toWrite: arrayBuffer, - returnStringsAsBuffers: false, - replies: [[[ - 'OK', - new ErrorReply('ERR'), - 0, - 'a', - [] - ]]] - }); - }); - - describe('as buffers', () => { - generateTests({ - toWrite: arrayBuffer, - returnStringsAsBuffers: true, - replies: [[[ - Buffer.from('OK'), - new ErrorReply('ERR'), - 0, - Buffer.from('a'), - [] - ]]] - }); - }); - }); -}); diff --git a/packages/client/lib/client/RESP2/decoder.ts b/packages/client/lib/client/RESP2/decoder.ts deleted file mode 100644 index 525f118bf30..00000000000 --- a/packages/client/lib/client/RESP2/decoder.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { ErrorReply } from '../../errors'; -import { Composer } from './composers/interface'; -import BufferComposer from './composers/buffer'; -import StringComposer from './composers/string'; - -// RESP2 specification -// https://redis.io/topics/protocol - -enum Types { - SIMPLE_STRING = 43, // + - ERROR = 45, // - - INTEGER = 58, // : - BULK_STRING = 36, // $ - ARRAY = 42 // * -} - -enum ASCII { - CR = 13, // \r - ZERO = 48, - MINUS = 45 -} - -export type Reply = string | Buffer | ErrorReply | number | null | Array; - -type ArrayReply = Array | null; - -export type ReturnStringsAsBuffers = () => boolean; - -interface RESP2Options { - returnStringsAsBuffers: ReturnStringsAsBuffers; - onReply(reply: Reply): unknown; -} - -interface ArrayInProcess { - array: Array; - pushCounter: number; -} - -// Using TypeScript `private` and not the build-in `#` to avoid __classPrivateFieldGet and __classPrivateFieldSet - -export default class RESP2Decoder { - constructor(private options: RESP2Options) {} - - private cursor = 0; - - private type?: Types; - - private bufferComposer = new BufferComposer(); - - private stringComposer = new StringComposer(); - - private currentStringComposer: BufferComposer | StringComposer = this.stringComposer; - - reset() { - this.cursor = 0; - this.type = undefined; - this.bufferComposer.reset(); - this.stringComposer.reset(); - this.currentStringComposer = this.stringComposer; - } - - write(chunk: Buffer): void { - while (this.cursor < chunk.length) { - if (!this.type) { - this.currentStringComposer = this.options.returnStringsAsBuffers() ? - this.bufferComposer : - this.stringComposer; - - this.type = chunk[this.cursor]; - if (++this.cursor >= chunk.length) break; - } - - const reply = this.parseType(chunk, this.type); - if (reply === undefined) break; - - this.type = undefined; - this.options.onReply(reply); - } - - this.cursor -= chunk.length; - } - - private parseType(chunk: Buffer, type: Types, arraysToKeep?: number): Reply | undefined { - switch (type) { - case Types.SIMPLE_STRING: - return this.parseSimpleString(chunk); - - case Types.ERROR: - return this.parseError(chunk); - - case Types.INTEGER: - return this.parseInteger(chunk); - - case Types.BULK_STRING: - return this.parseBulkString(chunk); - - case Types.ARRAY: - return this.parseArray(chunk, arraysToKeep); - } - } - - private compose< - C extends Composer, - T = C extends Composer ? TT : never - >( - chunk: Buffer, - composer: C - ): T | undefined { - for (let i = this.cursor; i < chunk.length; i++) { - if (chunk[i] === ASCII.CR) { - const reply = composer.end( - chunk.subarray(this.cursor, i) - ); - this.cursor = i + 2; - return reply; - } - } - - const toWrite = chunk.subarray(this.cursor); - composer.write(toWrite); - this.cursor = chunk.length; - } - - private parseSimpleString(chunk: Buffer): string | Buffer | undefined { - return this.compose(chunk, this.currentStringComposer); - } - - private parseError(chunk: Buffer): ErrorReply | undefined { - const message = this.compose(chunk, this.stringComposer); - if (message !== undefined) { - return new ErrorReply(message); - } - } - - private integer = 0; - - private isNegativeInteger?: boolean; - - private parseInteger(chunk: Buffer): number | undefined { - if (this.isNegativeInteger === undefined) { - this.isNegativeInteger = chunk[this.cursor] === ASCII.MINUS; - if (this.isNegativeInteger && ++this.cursor === chunk.length) return; - } - - do { - const byte = chunk[this.cursor]; - if (byte === ASCII.CR) { - const integer = this.isNegativeInteger ? -this.integer : this.integer; - this.integer = 0; - this.isNegativeInteger = undefined; - this.cursor += 2; - return integer; - } - - this.integer = this.integer * 10 + byte - ASCII.ZERO; - } while (++this.cursor < chunk.length); - } - - private bulkStringRemainingLength?: number; - - private parseBulkString(chunk: Buffer): string | Buffer | null | undefined { - if (this.bulkStringRemainingLength === undefined) { - const length = this.parseInteger(chunk); - if (length === undefined) return; - if (length === -1) return null; - - this.bulkStringRemainingLength = length; - - if (this.cursor >= chunk.length) return; - } - - const end = this.cursor + this.bulkStringRemainingLength; - if (chunk.length >= end) { - const reply = this.currentStringComposer.end( - chunk.subarray(this.cursor, end) - ); - this.bulkStringRemainingLength = undefined; - this.cursor = end + 2; - return reply; - } - - const toWrite = chunk.subarray(this.cursor); - this.currentStringComposer.write(toWrite); - this.bulkStringRemainingLength -= toWrite.length; - this.cursor = chunk.length; - } - - private arraysInProcess: Array = []; - - private initializeArray = false; - - private arrayItemType?: Types; - - private parseArray(chunk: Buffer, arraysToKeep = 0): ArrayReply | undefined { - if (this.initializeArray || this.arraysInProcess.length === arraysToKeep) { - const length = this.parseInteger(chunk); - if (length === undefined) { - this.initializeArray = true; - return undefined; - } - - this.initializeArray = false; - this.arrayItemType = undefined; - - if (length === -1) { - return this.returnArrayReply(null, arraysToKeep, chunk); - } else if (length === 0) { - return this.returnArrayReply([], arraysToKeep, chunk); - } - - this.arraysInProcess.push({ - array: new Array(length), - pushCounter: 0 - }); - } - - while (this.cursor < chunk.length) { - if (!this.arrayItemType) { - this.arrayItemType = chunk[this.cursor]; - - if (++this.cursor >= chunk.length) break; - } - - const item = this.parseType( - chunk, - this.arrayItemType, - arraysToKeep + 1 - ); - if (item === undefined) break; - - this.arrayItemType = undefined; - - const reply = this.pushArrayItem(item, arraysToKeep); - if (reply !== undefined) return reply; - } - } - - private returnArrayReply(reply: ArrayReply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined { - if (this.arraysInProcess.length <= arraysToKeep) return reply; - - return this.pushArrayItem(reply, arraysToKeep, chunk); - } - - private pushArrayItem(item: Reply, arraysToKeep: number, chunk?: Buffer): ArrayReply | undefined { - const to = this.arraysInProcess[this.arraysInProcess.length - 1]!; - to.array[to.pushCounter] = item; - if (++to.pushCounter === to.array.length) { - return this.returnArrayReply( - this.arraysInProcess.pop()!.array, - arraysToKeep, - chunk - ); - } else if (chunk && chunk.length > this.cursor) { - return this.parseArray(chunk, arraysToKeep); - } - } -} diff --git a/packages/client/lib/client/RESP2/encoder.spec.ts b/packages/client/lib/client/RESP2/encoder.spec.ts deleted file mode 100644 index 486259472a4..00000000000 --- a/packages/client/lib/client/RESP2/encoder.spec.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { strict as assert } from 'assert'; -import { describe } from 'mocha'; -import encodeCommand from './encoder'; - -describe('RESP2 Encoder', () => { - it('1 byte', () => { - assert.deepEqual( - encodeCommand(['a', 'z']), - ['*2\r\n$1\r\na\r\n$1\r\nz\r\n'] - ); - }); - - it('2 bytes', () => { - assert.deepEqual( - encodeCommand(['א', 'ת']), - ['*2\r\n$2\r\nא\r\n$2\r\nת\r\n'] - ); - }); - - it('4 bytes', () => { - assert.deepEqual( - [...encodeCommand(['🐣', '🐤'])], - ['*2\r\n$4\r\n🐣\r\n$4\r\n🐤\r\n'] - ); - }); - - it('buffer', () => { - assert.deepEqual( - encodeCommand([Buffer.from('string')]), - ['*1\r\n$6\r\n', Buffer.from('string'), '\r\n'] - ); - }); -}); diff --git a/packages/client/lib/client/RESP2/encoder.ts b/packages/client/lib/client/RESP2/encoder.ts deleted file mode 100644 index 217fbc714bb..00000000000 --- a/packages/client/lib/client/RESP2/encoder.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { RedisCommandArgument, RedisCommandArguments } from '../../commands'; - -const CRLF = '\r\n'; - -export default function encodeCommand(args: RedisCommandArguments): Array { - const toWrite: Array = []; - - let strings = '*' + args.length + CRLF; - - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - if (typeof arg === 'string') { - strings += '$' + Buffer.byteLength(arg) + CRLF + arg + CRLF; - } else if (arg instanceof Buffer) { - toWrite.push( - strings + '$' + arg.length.toString() + CRLF, - arg - ); - strings = CRLF; - } else { - throw new TypeError('Invalid argument type'); - } - } - - toWrite.push(strings); - - return toWrite; -} diff --git a/packages/client/lib/client/commands-queue.ts b/packages/client/lib/client/commands-queue.ts index 7fffed86580..a4029779fc8 100644 --- a/packages/client/lib/client/commands-queue.ts +++ b/packages/client/lib/client/commands-queue.ts @@ -1,263 +1,426 @@ -import * as LinkedList from 'yallist'; -import { AbortError, ErrorReply } from '../errors'; -import { RedisCommandArguments, RedisCommandRawReply } from '../commands'; -import RESP2Decoder from './RESP2/decoder'; -import encodeCommand from './RESP2/encoder'; +import { SinglyLinkedList, DoublyLinkedNode, DoublyLinkedList } from './linked-list'; +import encodeCommand from '../RESP/encoder'; +import { Decoder, PUSH_TYPE_MAPPING, RESP_TYPES } from '../RESP/decoder'; +import { CommandArguments, TypeMapping, ReplyUnion, RespVersions } from '../RESP/types'; import { ChannelListeners, PubSub, PubSubCommand, PubSubListener, PubSubType, PubSubTypeListeners } from './pub-sub'; - -export interface QueueCommandOptions { - asap?: boolean; - chainId?: symbol; - signal?: AbortSignal; - returnBuffers?: boolean; +import { AbortError, ErrorReply } from '../errors'; +import { MonitorCallback } from '.'; + +export interface CommandOptions { + chainId?: symbol; + asap?: boolean; + abortSignal?: AbortSignal; + /** + * Maps between RESP and JavaScript types + */ + typeMapping?: T; } -export interface CommandWaitingToBeSent extends CommandWaitingForReply { - args: RedisCommandArguments; - chainId?: symbol; - abort?: { - signal: AbortSignal; - listener(): void; - }; +export interface CommandToWrite extends CommandWaitingForReply { + args: CommandArguments; + chainId: symbol | undefined; + abort: { + signal: AbortSignal; + listener: () => unknown; + } | undefined; } interface CommandWaitingForReply { - resolve(reply?: unknown): void; - reject(err: unknown): void; - channelsCounter?: number; - returnBuffers?: boolean; + resolve(reply?: unknown): void; + reject(err: unknown): void; + channelsCounter: number | undefined; + typeMapping: TypeMapping | undefined; } -const PONG = Buffer.from('pong'); - export type OnShardedChannelMoved = (channel: string, listeners: ChannelListeners) => void; -export default class RedisCommandsQueue { - static #flushQueue(queue: LinkedList, err: Error): void { - while (queue.length) { - queue.shift()!.reject(err); - } - } - - readonly #maxLength: number | null | undefined; - readonly #waitingToBeSent = new LinkedList(); - readonly #waitingForReply = new LinkedList(); - readonly #onShardedChannelMoved: OnShardedChannelMoved; +const PONG = Buffer.from('pong'), + RESET = Buffer.from('RESET'); - readonly #pubSub = new PubSub(); +const RESP2_PUSH_TYPE_MAPPING = { + ...PUSH_TYPE_MAPPING, + [RESP_TYPES.SIMPLE_STRING]: Buffer +}; - get isPubSubActive() { - return this.#pubSub.isActive; +export default class RedisCommandsQueue { + readonly #respVersion; + readonly #maxLength; + readonly #toWrite = new DoublyLinkedList(); + readonly #waitingForReply = new SinglyLinkedList(); + readonly #onShardedChannelMoved; + #chainInExecution: symbol | undefined; + readonly decoder; + readonly #pubSub = new PubSub(); + + get isPubSubActive() { + return this.#pubSub.isActive; + } + + constructor( + respVersion: RespVersions, + maxLength: number | null | undefined, + onShardedChannelMoved: OnShardedChannelMoved + ) { + this.#respVersion = respVersion; + this.#maxLength = maxLength; + this.#onShardedChannelMoved = onShardedChannelMoved; + this.decoder = this.#initiateDecoder(); + } + + #onReply(reply: ReplyUnion) { + this.#waitingForReply.shift()!.resolve(reply); + } + + #onErrorReply(err: ErrorReply) { + this.#waitingForReply.shift()!.reject(err); + } + + #onPush(push: Array) { + // TODO: type + if (this.#pubSub.handleMessageReply(push)) return true; + + const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(push); + if (isShardedUnsubscribe && !this.#waitingForReply.length) { + const channel = push[1].toString(); + this.#onShardedChannelMoved( + channel, + this.#pubSub.removeShardedListeners(channel) + ); + return true; + } else if (isShardedUnsubscribe || PubSub.isStatusReply(push)) { + const head = this.#waitingForReply.head!.value; + if ( + (Number.isNaN(head.channelsCounter!) && push[2] === 0) || + --head.channelsCounter! === 0 + ) { + this.#waitingForReply.shift()!.resolve(); + } + return true; } + } - #chainInExecution: symbol | undefined; + #getTypeMapping() { + return this.#waitingForReply.head!.value.typeMapping ?? {}; + } + + #initiateDecoder() { + return new Decoder({ + onReply: reply => this.#onReply(reply), + onErrorReply: err => this.#onErrorReply(err), + onPush: push => { + if (!this.#onPush(push)) { - #decoder = new RESP2Decoder({ - returnStringsAsBuffers: () => { - return !!this.#waitingForReply.head?.value.returnBuffers || - this.#pubSub.isActive; - }, - onReply: reply => { - if (this.#pubSub.isActive && Array.isArray(reply)) { - if (this.#pubSub.handleMessageReply(reply as Array)) return; - - const isShardedUnsubscribe = PubSub.isShardedUnsubscribe(reply as Array); - if (isShardedUnsubscribe && !this.#waitingForReply.length) { - const channel = (reply[1] as Buffer).toString(); - this.#onShardedChannelMoved( - channel, - this.#pubSub.removeShardedListeners(channel) - ); - return; - } else if (isShardedUnsubscribe || PubSub.isStatusReply(reply as Array)) { - const head = this.#waitingForReply.head!.value; - if ( - (Number.isNaN(head.channelsCounter!) && reply[2] === 0) || - --head.channelsCounter! === 0 - ) { - this.#waitingForReply.shift()!.resolve(); - } - return; - } - if (PONG.equals(reply[0] as Buffer)) { - const { resolve, returnBuffers } = this.#waitingForReply.shift()!, - buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer; - resolve(returnBuffers ? buffer : buffer.toString()); - return; - } - } - - const { resolve, reject } = this.#waitingForReply.shift()!; - if (reply instanceof ErrorReply) { - reject(reply); - } else { - resolve(reply); - } } + }, + getTypeMapping: () => this.#getTypeMapping() }); - - constructor( - maxLength: number | null | undefined, - onShardedChannelMoved: OnShardedChannelMoved - ) { - this.#maxLength = maxLength; - this.#onShardedChannelMoved = onShardedChannelMoved; + } + + addCommand( + args: CommandArguments, + options?: CommandOptions + ): Promise { + if (this.#maxLength && this.#toWrite.length + this.#waitingForReply.length >= this.#maxLength) { + return Promise.reject(new Error('The queue is full')); + } else if (options?.abortSignal?.aborted) { + return Promise.reject(new AbortError()); } - addCommand(args: RedisCommandArguments, options?: QueueCommandOptions): Promise { - 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) => { + let node: DoublyLinkedNode; + const value: CommandToWrite = { + args, + chainId: options?.chainId, + abort: undefined, + resolve, + reject, + channelsCounter: undefined, + typeMapping: options?.typeMapping + }; + + const signal = options?.abortSignal; + if (signal) { + value.abort = { + signal, + listener: () => { + this.#toWrite.remove(node); + value.reject(new AbortError()); + } + }; + signal.addEventListener('abort', value.abort.listener, { once: true }); + } + + node = this.#toWrite.add(value, options?.asap); + }); + } + + #addPubSubCommand(command: PubSubCommand, asap = false, chainId?: symbol) { + return new Promise((resolve, reject) => { + this.#toWrite.add({ + args: command.args, + chainId, + abort: undefined, + resolve() { + command.resolve(); + resolve(); + }, + reject(err) { + command.reject?.(); + reject(err); + }, + channelsCounter: command.channelsCounter, + typeMapping: PUSH_TYPE_MAPPING + }, asap); + }); + } + + #setupPubSubHandler() { + // RESP3 uses `onPush` to handle PubSub, so no need to modify `onReply` + if (this.#respVersion !== 2) return; + + this.decoder.onReply = (reply => { + if (Array.isArray(reply)) { + if (this.#onPush(reply)) return; + + if (PONG.equals(reply[0] as Buffer)) { + const { resolve, typeMapping } = this.#waitingForReply.shift()!, + buffer = ((reply[1] as Buffer).length === 0 ? reply[0] : reply[1]) as Buffer; + resolve(typeMapping?.[RESP_TYPES.SIMPLE_STRING] === Buffer ? buffer : buffer.toString()); + return; } - - return new Promise((resolve, reject) => { - const node = new LinkedList.Node({ - args, - chainId: options?.chainId, - returnBuffers: options?.returnBuffers, - resolve, - reject - }); - - if (options?.signal) { - const listener = () => { - this.#waitingToBeSent.removeNode(node); - node.value.reject(new AbortError()); - }; - node.value.abort = { - signal: options.signal, - listener - }; - // AbortSignal type is incorrent - (options.signal as any).addEventListener('abort', listener, { - once: true - }); - } - - if (options?.asap) { - this.#waitingToBeSent.unshiftNode(node); - } else { - this.#waitingToBeSent.pushNode(node); - } - }); - } - - subscribe( - type: PubSubType, - channels: string | Array, - listener: PubSubListener, - returnBuffers?: T - ) { - return this.#pushPubSubCommand( - this.#pubSub.subscribe(type, channels, listener, returnBuffers) - ); - } - - unsubscribe( - type: PubSubType, - channels?: string | Array, - listener?: PubSubListener, - returnBuffers?: T - ) { - return this.#pushPubSubCommand( - this.#pubSub.unsubscribe(type, channels, listener, returnBuffers) - ); - } - - resubscribe(): Promise | undefined { - const commands = this.#pubSub.resubscribe(); - if (!commands.length) return; - - return Promise.all( - commands.map(command => this.#pushPubSubCommand(command)) - ); + } + + return this.#onReply(reply); + }) as Decoder['onReply']; + this.decoder.getTypeMapping = () => RESP2_PUSH_TYPE_MAPPING; + } + + subscribe( + type: PubSubType, + channels: string | Array, + listener: PubSubListener, + returnBuffers?: T + ) { + const command = this.#pubSub.subscribe(type, channels, listener, returnBuffers); + if (!command) return; + + this.#setupPubSubHandler(); + return this.#addPubSubCommand(command); + } + + #resetDecoderCallbacks() { + this.decoder.onReply = (reply => this.#onReply(reply)) as Decoder['onReply']; + this.decoder.getTypeMapping = () => this.#getTypeMapping(); + } + + unsubscribe( + type: PubSubType, + channels?: string | Array, + listener?: PubSubListener, + returnBuffers?: T + ) { + const command = this.#pubSub.unsubscribe(type, channels, listener, returnBuffers); + if (!command) return; + + if (command && this.#respVersion === 2) { + // RESP2 modifies `onReply` to handle PubSub (see #setupPubSubHandler) + const { resolve } = command; + command.resolve = () => { + if (!this.#pubSub.isActive) { + this.#resetDecoderCallbacks(); + } + + resolve(); + }; } - extendPubSubChannelListeners( - type: PubSubType, - channel: string, - listeners: ChannelListeners - ) { - return this.#pushPubSubCommand( - this.#pubSub.extendChannelListeners(type, channel, listeners) - ); + return this.#addPubSubCommand(command); + } + + resubscribe(chainId?: symbol) { + const commands = this.#pubSub.resubscribe(); + if (!commands.length) return; + + this.#setupPubSubHandler(); + return Promise.all( + commands.map(command => this.#addPubSubCommand(command, true, chainId)) + ); + } + + extendPubSubChannelListeners( + type: PubSubType, + channel: string, + listeners: ChannelListeners + ) { + const command = this.#pubSub.extendChannelListeners(type, channel, listeners); + if (!command) return; + + this.#setupPubSubHandler(); + return this.#addPubSubCommand(command); + } + + extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) { + const command = this.#pubSub.extendTypeListeners(type, listeners); + if (!command) return; + + this.#setupPubSubHandler(); + return this.#addPubSubCommand(command); + } + + getPubSubListeners(type: PubSubType) { + return this.#pubSub.listeners[type]; + } + + monitor(callback: MonitorCallback, options?: CommandOptions) { + return new Promise((resolve, reject) => { + const typeMapping = options?.typeMapping ?? {}; + this.#toWrite.add({ + args: ['MONITOR'], + chainId: options?.chainId, + abort: undefined, + // using `resolve` instead of using `.then`/`await` to make sure it'll be called before processing the next reply + resolve: () => { + // after running `MONITOR` only `MONITOR` and `RESET` replies are expected + // any other command should cause an error + + // if `RESET` already overrides `onReply`, set monitor as it's fallback + if (this.#resetFallbackOnReply) { + this.#resetFallbackOnReply = callback; + } else { + this.decoder.onReply = callback; + } + + this.decoder.getTypeMapping = () => typeMapping; + resolve(); + }, + reject, + channelsCounter: undefined, + typeMapping + }, options?.asap); + }); + } + + resetDecoder() { + this.#resetDecoderCallbacks(); + this.decoder.reset(); + } + + #resetFallbackOnReply?: Decoder['onReply']; + + async reset(chainId: symbol, typeMapping?: T) { + return new Promise((resolve, reject) => { + // overriding onReply to handle `RESET` while in `MONITOR` or PubSub mode + this.#resetFallbackOnReply = this.decoder.onReply; + this.decoder.onReply = (reply => { + if ( + (typeof reply === 'string' && reply === 'RESET') || + (reply instanceof Buffer && RESET.equals(reply)) + ) { + this.#resetDecoderCallbacks(); + this.#resetFallbackOnReply = undefined; + this.#pubSub.reset(); + + this.#waitingForReply.shift()!.resolve(reply); + return; + } + + this.#resetFallbackOnReply!(reply); + }) as Decoder['onReply']; + + this.#toWrite.push({ + args: ['RESET'], + chainId, + abort: undefined, + resolve, + reject, + channelsCounter: undefined, + typeMapping + }); + }); + } + + isWaitingToWrite() { + return this.#toWrite.length > 0; + } + + *commandsToWrite() { + let toSend = this.#toWrite.shift(); + while (toSend) { + let encoded: CommandArguments; + try { + encoded = encodeCommand(toSend.args); + } catch (err) { + toSend.reject(err); + toSend = this.#toWrite.shift(); + continue; + } + + // TODO reuse `toSend` or create new object? + (toSend as any).args = undefined; + if (toSend.abort) { + RedisCommandsQueue.#removeAbortListener(toSend); + toSend.abort = undefined; + } + this.#chainInExecution = toSend.chainId; + toSend.chainId = undefined; + this.#waitingForReply.push(toSend); + + yield encoded; + toSend = this.#toWrite.shift(); } + } - extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) { - return this.#pushPubSubCommand( - this.#pubSub.extendTypeListeners(type, listeners) - ); + #flushWaitingForReply(err: Error): void { + for (const node of this.#waitingForReply) { + node.reject(err); } + this.#waitingForReply.reset(); + } - getPubSubListeners(type: PubSubType) { - return this.#pubSub.getTypeListeners(type); - } + static #removeAbortListener(command: CommandToWrite) { + command.abort!.signal.removeEventListener('abort', command.abort!.listener); + } - #pushPubSubCommand(command: PubSubCommand) { - if (command === undefined) return; - - return new Promise((resolve, reject) => { - this.#waitingToBeSent.push({ - args: command.args, - channelsCounter: command.channelsCounter, - returnBuffers: true, - resolve: () => { - command.resolve(); - resolve(); - }, - reject: err => { - command.reject?.(); - reject(err); - } - }); - }); + static #flushToWrite(toBeSent: CommandToWrite, err: Error) { + if (toBeSent.abort) { + RedisCommandsQueue.#removeAbortListener(toBeSent); } + + toBeSent.reject(err); + } - getCommandToSend(): RedisCommandArguments | undefined { - const toSend = this.#waitingToBeSent.shift(); - if (!toSend) return; + flushWaitingForReply(err: Error): void { + this.resetDecoder(); + this.#pubSub.reset(); - let encoded: RedisCommandArguments; - try { - encoded = encodeCommand(toSend.args); - } catch (err) { - toSend.reject(err); - return; - } + this.#flushWaitingForReply(err); - this.#waitingForReply.push({ - resolve: toSend.resolve, - reject: toSend.reject, - channelsCounter: toSend.channelsCounter, - returnBuffers: toSend.returnBuffers - }); - this.#chainInExecution = toSend.chainId; - return encoded; - } + if (!this.#chainInExecution) return; - onReplyChunk(chunk: Buffer): void { - this.#decoder.write(chunk); + while (this.#toWrite.head?.value.chainId === this.#chainInExecution) { + RedisCommandsQueue.#flushToWrite( + this.#toWrite.shift()!, + err + ); } - flushWaitingForReply(err: Error): void { - this.#decoder.reset(); - this.#pubSub.reset(); - RedisCommandsQueue.#flushQueue(this.#waitingForReply, err); - - if (!this.#chainInExecution) return; - - while (this.#waitingToBeSent.head?.value.chainId === this.#chainInExecution) { - this.#waitingToBeSent.shift(); - } - - this.#chainInExecution = undefined; - } + this.#chainInExecution = undefined; + } - flushAll(err: Error): void { - this.#decoder.reset(); - this.#pubSub.reset(); - RedisCommandsQueue.#flushQueue(this.#waitingForReply, err); - RedisCommandsQueue.#flushQueue(this.#waitingToBeSent, err); + flushAll(err: Error): void { + this.resetDecoder(); + this.#pubSub.reset(); + this.#flushWaitingForReply(err); + for (const node of this.#toWrite) { + RedisCommandsQueue.#flushToWrite(node, err); } + this.#toWrite.reset(); + } + + isEmpty() { + return ( + this.#toWrite.length === 0 && + this.#waitingForReply.length === 0 + ); + } } diff --git a/packages/client/lib/client/commands.ts b/packages/client/lib/client/commands.ts deleted file mode 100644 index 76ae5d73735..00000000000 --- a/packages/client/lib/client/commands.ts +++ /dev/null @@ -1,374 +0,0 @@ -import CLUSTER_COMMANDS from '../cluster/commands'; -import * as ACL_CAT from '../commands/ACL_CAT'; -import * as ACL_DELUSER from '../commands/ACL_DELUSER'; -import * as ACL_DRYRUN from '../commands/ACL_DRYRUN'; -import * as ACL_GENPASS from '../commands/ACL_GENPASS'; -import * as ACL_GETUSER from '../commands/ACL_GETUSER'; -import * as ACL_LIST from '../commands/ACL_LIST'; -import * as ACL_LOAD from '../commands/ACL_LOAD'; -import * as ACL_LOG_RESET from '../commands/ACL_LOG_RESET'; -import * as ACL_LOG from '../commands/ACL_LOG'; -import * as ACL_SAVE from '../commands/ACL_SAVE'; -import * as ACL_SETUSER from '../commands/ACL_SETUSER'; -import * as ACL_USERS from '../commands/ACL_USERS'; -import * as ACL_WHOAMI from '../commands/ACL_WHOAMI'; -import * as ASKING from '../commands/ASKING'; -import * as AUTH from '../commands/AUTH'; -import * as BGREWRITEAOF from '../commands/BGREWRITEAOF'; -import * as BGSAVE from '../commands/BGSAVE'; -import * as CLIENT_CACHING from '../commands/CLIENT_CACHING'; -import * as CLIENT_GETNAME from '../commands/CLIENT_GETNAME'; -import * as CLIENT_GETREDIR from '../commands/CLIENT_GETREDIR'; -import * as CLIENT_ID from '../commands/CLIENT_ID'; -import * as CLIENT_KILL from '../commands/CLIENT_KILL'; -import * as CLIENT_LIST from '../commands/CLIENT_LIST'; -import * as CLIENT_NO_EVICT from '../commands/CLIENT_NO-EVICT'; -import * as CLIENT_NO_TOUCH from '../commands/CLIENT_NO-TOUCH'; -import * as CLIENT_PAUSE from '../commands/CLIENT_PAUSE'; -import * as CLIENT_SETNAME from '../commands/CLIENT_SETNAME'; -import * as CLIENT_TRACKING from '../commands/CLIENT_TRACKING'; -import * as CLIENT_TRACKINGINFO from '../commands/CLIENT_TRACKINGINFO'; -import * as CLIENT_UNPAUSE from '../commands/CLIENT_UNPAUSE'; -import * as CLIENT_INFO from '../commands/CLIENT_INFO'; -import * as CLUSTER_ADDSLOTS from '../commands/CLUSTER_ADDSLOTS'; -import * as CLUSTER_ADDSLOTSRANGE from '../commands/CLUSTER_ADDSLOTSRANGE'; -import * as CLUSTER_BUMPEPOCH from '../commands/CLUSTER_BUMPEPOCH'; -import * as CLUSTER_COUNT_FAILURE_REPORTS from '../commands/CLUSTER_COUNT-FAILURE-REPORTS'; -import * as CLUSTER_COUNTKEYSINSLOT from '../commands/CLUSTER_COUNTKEYSINSLOT'; -import * as CLUSTER_DELSLOTS from '../commands/CLUSTER_DELSLOTS'; -import * as CLUSTER_DELSLOTSRANGE from '../commands/CLUSTER_DELSLOTSRANGE'; -import * as CLUSTER_FAILOVER from '../commands/CLUSTER_FAILOVER'; -import * as CLUSTER_FLUSHSLOTS from '../commands/CLUSTER_FLUSHSLOTS'; -import * as CLUSTER_FORGET from '../commands/CLUSTER_FORGET'; -import * as CLUSTER_GETKEYSINSLOT from '../commands/CLUSTER_GETKEYSINSLOT'; -import * as CLUSTER_INFO from '../commands/CLUSTER_INFO'; -import * as CLUSTER_KEYSLOT from '../commands/CLUSTER_KEYSLOT'; -import * as CLUSTER_LINKS from '../commands/CLUSTER_LINKS'; -import * as CLUSTER_MEET from '../commands/CLUSTER_MEET'; -import * as CLUSTER_MYID from '../commands/CLUSTER_MYID'; -import * as CLUSTER_MYSHARDID from '../commands/CLUSTER_MYSHARDID'; -import * as CLUSTER_NODES from '../commands/CLUSTER_NODES'; -import * as CLUSTER_REPLICAS from '../commands/CLUSTER_REPLICAS'; -import * as CLUSTER_REPLICATE from '../commands/CLUSTER_REPLICATE'; -import * as CLUSTER_RESET from '../commands/CLUSTER_RESET'; -import * as CLUSTER_SAVECONFIG from '../commands/CLUSTER_SAVECONFIG'; -import * as CLUSTER_SET_CONFIG_EPOCH from '../commands/CLUSTER_SET-CONFIG-EPOCH'; -import * as CLUSTER_SETSLOT from '../commands/CLUSTER_SETSLOT'; -import * as CLUSTER_SLOTS from '../commands/CLUSTER_SLOTS'; -import * as COMMAND_COUNT from '../commands/COMMAND_COUNT'; -import * as COMMAND_GETKEYS from '../commands/COMMAND_GETKEYS'; -import * as COMMAND_GETKEYSANDFLAGS from '../commands/COMMAND_GETKEYSANDFLAGS'; -import * as COMMAND_INFO from '../commands/COMMAND_INFO'; -import * as COMMAND_LIST from '../commands/COMMAND_LIST'; -import * as COMMAND from '../commands/COMMAND'; -import * as CONFIG_GET from '../commands/CONFIG_GET'; -import * as CONFIG_RESETASTAT from '../commands/CONFIG_RESETSTAT'; -import * as CONFIG_REWRITE from '../commands/CONFIG_REWRITE'; -import * as CONFIG_SET from '../commands/CONFIG_SET'; -import * as DBSIZE from '../commands/DBSIZE'; -import * as DISCARD from '../commands/DISCARD'; -import * as ECHO from '../commands/ECHO'; -import * as FAILOVER from '../commands/FAILOVER'; -import * as FLUSHALL from '../commands/FLUSHALL'; -import * as FLUSHDB from '../commands/FLUSHDB'; -import * as FUNCTION_DELETE from '../commands/FUNCTION_DELETE'; -import * as FUNCTION_DUMP from '../commands/FUNCTION_DUMP'; -import * as FUNCTION_FLUSH from '../commands/FUNCTION_FLUSH'; -import * as FUNCTION_KILL from '../commands/FUNCTION_KILL'; -import * as FUNCTION_LIST_WITHCODE from '../commands/FUNCTION_LIST_WITHCODE'; -import * as FUNCTION_LIST from '../commands/FUNCTION_LIST'; -import * as FUNCTION_LOAD from '../commands/FUNCTION_LOAD'; -import * as FUNCTION_RESTORE from '../commands/FUNCTION_RESTORE'; -import * as FUNCTION_STATS from '../commands/FUNCTION_STATS'; -import * as HELLO from '../commands/HELLO'; -import * as INFO from '../commands/INFO'; -import * as KEYS from '../commands/KEYS'; -import * as LASTSAVE from '../commands/LASTSAVE'; -import * as LATENCY_DOCTOR from '../commands/LATENCY_DOCTOR'; -import * as LATENCY_GRAPH from '../commands/LATENCY_GRAPH'; -import * as LATENCY_HISTORY from '../commands/LATENCY_HISTORY'; -import * as LATENCY_LATEST from '../commands/LATENCY_LATEST'; -import * as LOLWUT from '../commands/LOLWUT'; -import * as MEMORY_DOCTOR from '../commands/MEMORY_DOCTOR'; -import * as MEMORY_MALLOC_STATS from '../commands/MEMORY_MALLOC-STATS'; -import * as MEMORY_PURGE from '../commands/MEMORY_PURGE'; -import * as MEMORY_STATS from '../commands/MEMORY_STATS'; -import * as MEMORY_USAGE from '../commands/MEMORY_USAGE'; -import * as MODULE_LIST from '../commands/MODULE_LIST'; -import * as MODULE_LOAD from '../commands/MODULE_LOAD'; -import * as MODULE_UNLOAD from '../commands/MODULE_UNLOAD'; -import * as MOVE from '../commands/MOVE'; -import * as PING from '../commands/PING'; -import * as PUBSUB_CHANNELS from '../commands/PUBSUB_CHANNELS'; -import * as PUBSUB_NUMPAT from '../commands/PUBSUB_NUMPAT'; -import * as PUBSUB_NUMSUB from '../commands/PUBSUB_NUMSUB'; -import * as PUBSUB_SHARDCHANNELS from '../commands/PUBSUB_SHARDCHANNELS'; -import * as PUBSUB_SHARDNUMSUB from '../commands/PUBSUB_SHARDNUMSUB'; -import * as RANDOMKEY from '../commands/RANDOMKEY'; -import * as READONLY from '../commands/READONLY'; -import * as READWRITE from '../commands/READWRITE'; -import * as REPLICAOF from '../commands/REPLICAOF'; -import * as RESTORE_ASKING from '../commands/RESTORE-ASKING'; -import * as ROLE from '../commands/ROLE'; -import * as SAVE from '../commands/SAVE'; -import * as SCAN from '../commands/SCAN'; -import * as SCRIPT_DEBUG from '../commands/SCRIPT_DEBUG'; -import * as SCRIPT_EXISTS from '../commands/SCRIPT_EXISTS'; -import * as SCRIPT_FLUSH from '../commands/SCRIPT_FLUSH'; -import * as SCRIPT_KILL from '../commands/SCRIPT_KILL'; -import * as SCRIPT_LOAD from '../commands/SCRIPT_LOAD'; -import * as SHUTDOWN from '../commands/SHUTDOWN'; -import * as SWAPDB from '../commands/SWAPDB'; -import * as TIME from '../commands/TIME'; -import * as UNWATCH from '../commands/UNWATCH'; -import * as WAIT from '../commands/WAIT'; - -export default { - ...CLUSTER_COMMANDS, - ACL_CAT, - aclCat: ACL_CAT, - ACL_DELUSER, - aclDelUser: ACL_DELUSER, - ACL_DRYRUN, - aclDryRun: ACL_DRYRUN, - ACL_GENPASS, - aclGenPass: ACL_GENPASS, - ACL_GETUSER, - aclGetUser: ACL_GETUSER, - ACL_LIST, - aclList: ACL_LIST, - ACL_LOAD, - aclLoad: ACL_LOAD, - ACL_LOG_RESET, - aclLogReset: ACL_LOG_RESET, - ACL_LOG, - aclLog: ACL_LOG, - ACL_SAVE, - aclSave: ACL_SAVE, - ACL_SETUSER, - aclSetUser: ACL_SETUSER, - ACL_USERS, - aclUsers: ACL_USERS, - ACL_WHOAMI, - aclWhoAmI: ACL_WHOAMI, - ASKING, - asking: ASKING, - AUTH, - auth: AUTH, - BGREWRITEAOF, - bgRewriteAof: BGREWRITEAOF, - BGSAVE, - bgSave: BGSAVE, - CLIENT_CACHING, - clientCaching: CLIENT_CACHING, - CLIENT_GETNAME, - clientGetName: CLIENT_GETNAME, - CLIENT_GETREDIR, - clientGetRedir: CLIENT_GETREDIR, - CLIENT_ID, - clientId: CLIENT_ID, - CLIENT_KILL, - clientKill: CLIENT_KILL, - 'CLIENT_NO-EVICT': CLIENT_NO_EVICT, - clientNoEvict: CLIENT_NO_EVICT, - 'CLIENT_NO-TOUCH': CLIENT_NO_TOUCH, - clientNoTouch: CLIENT_NO_TOUCH, - CLIENT_LIST, - clientList: CLIENT_LIST, - CLIENT_PAUSE, - clientPause: CLIENT_PAUSE, - CLIENT_SETNAME, - clientSetName: CLIENT_SETNAME, - CLIENT_TRACKING, - clientTracking: CLIENT_TRACKING, - CLIENT_TRACKINGINFO, - clientTrackingInfo: CLIENT_TRACKINGINFO, - CLIENT_UNPAUSE, - clientUnpause: CLIENT_UNPAUSE, - CLIENT_INFO, - clientInfo: CLIENT_INFO, - CLUSTER_ADDSLOTS, - clusterAddSlots: CLUSTER_ADDSLOTS, - CLUSTER_ADDSLOTSRANGE, - clusterAddSlotsRange: CLUSTER_ADDSLOTSRANGE, - CLUSTER_BUMPEPOCH, - clusterBumpEpoch: CLUSTER_BUMPEPOCH, - CLUSTER_COUNT_FAILURE_REPORTS, - clusterCountFailureReports: CLUSTER_COUNT_FAILURE_REPORTS, - CLUSTER_COUNTKEYSINSLOT, - clusterCountKeysInSlot: CLUSTER_COUNTKEYSINSLOT, - CLUSTER_DELSLOTS, - clusterDelSlots: CLUSTER_DELSLOTS, - CLUSTER_DELSLOTSRANGE, - clusterDelSlotsRange: CLUSTER_DELSLOTSRANGE, - CLUSTER_FAILOVER, - clusterFailover: CLUSTER_FAILOVER, - CLUSTER_FLUSHSLOTS, - clusterFlushSlots: CLUSTER_FLUSHSLOTS, - CLUSTER_FORGET, - clusterForget: CLUSTER_FORGET, - CLUSTER_GETKEYSINSLOT, - clusterGetKeysInSlot: CLUSTER_GETKEYSINSLOT, - CLUSTER_INFO, - clusterInfo: CLUSTER_INFO, - CLUSTER_KEYSLOT, - clusterKeySlot: CLUSTER_KEYSLOT, - CLUSTER_LINKS, - clusterLinks: CLUSTER_LINKS, - CLUSTER_MEET, - clusterMeet: CLUSTER_MEET, - CLUSTER_MYID, - clusterMyId: CLUSTER_MYID, - CLUSTER_MYSHARDID, - clusterMyShardId: CLUSTER_MYSHARDID, - CLUSTER_NODES, - clusterNodes: CLUSTER_NODES, - CLUSTER_REPLICAS, - clusterReplicas: CLUSTER_REPLICAS, - CLUSTER_REPLICATE, - clusterReplicate: CLUSTER_REPLICATE, - CLUSTER_RESET, - clusterReset: CLUSTER_RESET, - CLUSTER_SAVECONFIG, - clusterSaveConfig: CLUSTER_SAVECONFIG, - CLUSTER_SET_CONFIG_EPOCH, - clusterSetConfigEpoch: CLUSTER_SET_CONFIG_EPOCH, - CLUSTER_SETSLOT, - clusterSetSlot: CLUSTER_SETSLOT, - CLUSTER_SLOTS, - clusterSlots: CLUSTER_SLOTS, - COMMAND_COUNT, - commandCount: COMMAND_COUNT, - COMMAND_GETKEYS, - commandGetKeys: COMMAND_GETKEYS, - COMMAND_GETKEYSANDFLAGS, - commandGetKeysAndFlags: COMMAND_GETKEYSANDFLAGS, - COMMAND_INFO, - commandInfo: COMMAND_INFO, - COMMAND_LIST, - commandList: COMMAND_LIST, - COMMAND, - command: COMMAND, - CONFIG_GET, - configGet: CONFIG_GET, - CONFIG_RESETASTAT, - configResetStat: CONFIG_RESETASTAT, - CONFIG_REWRITE, - configRewrite: CONFIG_REWRITE, - CONFIG_SET, - configSet: CONFIG_SET, - DBSIZE, - dbSize: DBSIZE, - DISCARD, - discard: DISCARD, - ECHO, - echo: ECHO, - FAILOVER, - failover: FAILOVER, - FLUSHALL, - flushAll: FLUSHALL, - FLUSHDB, - flushDb: FLUSHDB, - FUNCTION_DELETE, - functionDelete: FUNCTION_DELETE, - FUNCTION_DUMP, - functionDump: FUNCTION_DUMP, - FUNCTION_FLUSH, - functionFlush: FUNCTION_FLUSH, - FUNCTION_KILL, - functionKill: FUNCTION_KILL, - FUNCTION_LIST_WITHCODE, - functionListWithCode: FUNCTION_LIST_WITHCODE, - FUNCTION_LIST, - functionList: FUNCTION_LIST, - FUNCTION_LOAD, - functionLoad: FUNCTION_LOAD, - FUNCTION_RESTORE, - functionRestore: FUNCTION_RESTORE, - FUNCTION_STATS, - functionStats: FUNCTION_STATS, - HELLO, - hello: HELLO, - INFO, - info: INFO, - KEYS, - keys: KEYS, - LASTSAVE, - lastSave: LASTSAVE, - LATENCY_DOCTOR, - latencyDoctor: LATENCY_DOCTOR, - LATENCY_GRAPH, - latencyGraph: LATENCY_GRAPH, - LATENCY_HISTORY, - latencyHistory: LATENCY_HISTORY, - LATENCY_LATEST, - latencyLatest: LATENCY_LATEST, - LOLWUT, - lolwut: LOLWUT, - MEMORY_DOCTOR, - memoryDoctor: MEMORY_DOCTOR, - 'MEMORY_MALLOC-STATS': MEMORY_MALLOC_STATS, - memoryMallocStats: MEMORY_MALLOC_STATS, - MEMORY_PURGE, - memoryPurge: MEMORY_PURGE, - MEMORY_STATS, - memoryStats: MEMORY_STATS, - MEMORY_USAGE, - memoryUsage: MEMORY_USAGE, - MODULE_LIST, - moduleList: MODULE_LIST, - MODULE_LOAD, - moduleLoad: MODULE_LOAD, - MODULE_UNLOAD, - moduleUnload: MODULE_UNLOAD, - MOVE, - move: MOVE, - PING, - ping: PING, - PUBSUB_CHANNELS, - pubSubChannels: PUBSUB_CHANNELS, - PUBSUB_NUMPAT, - pubSubNumPat: PUBSUB_NUMPAT, - PUBSUB_NUMSUB, - pubSubNumSub: PUBSUB_NUMSUB, - PUBSUB_SHARDCHANNELS, - pubSubShardChannels: PUBSUB_SHARDCHANNELS, - PUBSUB_SHARDNUMSUB, - pubSubShardNumSub: PUBSUB_SHARDNUMSUB, - RANDOMKEY, - randomKey: RANDOMKEY, - READONLY, - readonly: READONLY, - READWRITE, - readwrite: READWRITE, - REPLICAOF, - replicaOf: REPLICAOF, - 'RESTORE-ASKING': RESTORE_ASKING, - restoreAsking: RESTORE_ASKING, - ROLE, - role: ROLE, - SAVE, - save: SAVE, - SCAN, - scan: SCAN, - SCRIPT_DEBUG, - scriptDebug: SCRIPT_DEBUG, - SCRIPT_EXISTS, - scriptExists: SCRIPT_EXISTS, - SCRIPT_FLUSH, - scriptFlush: SCRIPT_FLUSH, - SCRIPT_KILL, - scriptKill: SCRIPT_KILL, - SCRIPT_LOAD, - scriptLoad: SCRIPT_LOAD, - SHUTDOWN, - shutdown: SHUTDOWN, - SWAPDB, - swapDb: SWAPDB, - TIME, - time: TIME, - UNWATCH, - unwatch: UNWATCH, - WAIT, - wait: WAIT -}; diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 7f93efaa1c3..50ed3d39da1 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -1,1076 +1,800 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; import RedisClient, { RedisClientType } from '.'; -import { RedisClientMultiCommandType } from './multi-command'; -import { RedisCommandRawReply, RedisModules, RedisFunctions, RedisScripts } from '../commands'; import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors'; import { defineScript } from '../lua-script'; import { spy } from 'sinon'; -import { once } from 'events'; -import { ClientKillFilters } from '../commands/CLIENT_KILL'; -import { promisify } from 'util'; - -import {version} from '../../package.json'; +import { once } from 'node:events'; +import { MATH_FUNCTION, loadMathFunction } from '../commands/FUNCTION_LOAD.spec'; +import { RESP_TYPES } from '../RESP/decoder'; +import { BlobStringReply, NumberReply } from '../RESP/types'; +import { SortedSetMember } from '../commands/generic-transformers'; export const SQUARE_SCRIPT = defineScript({ - SCRIPT: 'return ARGV[1] * ARGV[1];', - NUMBER_OF_KEYS: 0, - transformArguments(number: number): Array { - return [number.toString()]; - } + SCRIPT: + `local number = redis.call('GET', KEYS[1]) + return number * number`, + NUMBER_OF_KEYS: 1, + FIRST_KEY_INDEX: 0, + transformArguments(key: string) { + return [key]; + }, + transformReply: undefined as unknown as () => NumberReply }); -export const MATH_FUNCTION = { - name: 'math', - engine: 'LUA', - code: `#!LUA name=math - redis.register_function{ - function_name = "square", - callback = function(keys, args) return args[1] * args[1] end, - flags = { "no-writes" } - }`, - library: { - square: { - NAME: 'square', - IS_READ_ONLY: true, - NUMBER_OF_KEYS: 0, - transformArguments(number: number): Array { - return [number.toString()]; - } - } - } -}; - -export async function loadMathFunction( - client: RedisClientType -): Promise { - await client.functionLoad( - MATH_FUNCTION.code, - { REPLACE: true } - ); -} - describe('Client', () => { - describe('parseURL', () => { - it('redis://user:secret@localhost:6379/0', () => { - assert.deepEqual( - RedisClient.parseURL('redis://user:secret@localhost:6379/0'), - { - socket: { - host: 'localhost', - port: 6379 - }, - username: 'user', - password: 'secret', - database: 0 - } - ); - }); - - it('rediss://user:secret@localhost:6379/0', () => { - assert.deepEqual( - RedisClient.parseURL('rediss://user:secret@localhost:6379/0'), - { - socket: { - host: 'localhost', - port: 6379, - tls: true - }, - username: 'user', - password: 'secret', - database: 0 - } - ); - }); - - it('Invalid protocol', () => { - assert.throws( - () => RedisClient.parseURL('redi://user:secret@localhost:6379/0'), - TypeError - ); - }); - - it('Invalid pathname', () => { - assert.throws( - () => RedisClient.parseURL('redis://user:secret@localhost:6379/NaN'), - TypeError - ); - }); - - it('redis://localhost', () => { - assert.deepEqual( - RedisClient.parseURL('redis://localhost'), - { - socket: { - host: 'localhost', - } - } - ); - }); + describe('parseURL', () => { + it('redis://user:secret@localhost:6379/0', () => { + assert.deepEqual( + RedisClient.parseURL('redis://user:secret@localhost:6379/0'), + { + socket: { + host: 'localhost', + port: 6379 + }, + username: 'user', + password: 'secret', + database: 0 + } + ); }); - describe('connect', () => { - testUtils.testWithClient('connect should return the client instance', async client => { - try { - assert.equal(await client.connect(), client); - } finally { - if (client.isOpen) await client.disconnect(); - } - }, { - ...GLOBAL.SERVERS.PASSWORD, - disableClientSetup: true - }); - - testUtils.testWithClient('should set default lib name and version', async client => { - const clientInfo = await client.clientInfo(); - - assert.equal(clientInfo.libName, 'node-redis'); - assert.equal(clientInfo.libVer, version); - }, { - ...GLOBAL.SERVERS.PASSWORD, - minimumDockerVersion: [7, 2] - }); - - testUtils.testWithClient('disable sending lib name and version', async client => { - const clientInfo = await client.clientInfo(); - - assert.equal(clientInfo.libName, ''); - assert.equal(clientInfo.libVer, ''); - }, { - ...GLOBAL.SERVERS.PASSWORD, - clientOptions: { - ...GLOBAL.SERVERS.PASSWORD.clientOptions, - disableClientInfo: true - }, - minimumDockerVersion: [7, 2] - }); + it('rediss://user:secret@localhost:6379/0', () => { + assert.deepEqual( + RedisClient.parseURL('rediss://user:secret@localhost:6379/0'), + { + socket: { + host: 'localhost', + port: 6379, + tls: true + }, + username: 'user', + password: 'secret', + database: 0 + } + ); + }); - testUtils.testWithClient('send client name tag', async client => { - const clientInfo = await client.clientInfo(); - - assert.equal(clientInfo.libName, 'node-redis(test)'); - assert.equal(clientInfo.libVer, version); - }, { - ...GLOBAL.SERVERS.PASSWORD, - clientOptions: { - ...GLOBAL.SERVERS.PASSWORD.clientOptions, - clientInfoTag: "test" - }, - minimumDockerVersion: [7, 2] - }); + it('Invalid protocol', () => { + assert.throws( + () => RedisClient.parseURL('redi://user:secret@localhost:6379/0'), + TypeError + ); }); - describe('authentication', () => { - testUtils.testWithClient('Client should be authenticated', async client => { - assert.equal( - await client.ping(), - 'PONG' - ); - }, GLOBAL.SERVERS.PASSWORD); - - testUtils.testWithClient('should execute AUTH before SELECT', async client => { - assert.equal( - (await client.clientInfo()).db, - 2 - ); - }, { - ...GLOBAL.SERVERS.PASSWORD, - clientOptions: { - ...GLOBAL.SERVERS.PASSWORD.clientOptions, - database: 2 - }, - minimumDockerVersion: [6, 2] - }); + it('Invalid pathname', () => { + assert.throws( + () => RedisClient.parseURL('redis://user:secret@localhost:6379/NaN'), + TypeError + ); }); - testUtils.testWithClient('should set connection name', async client => { - assert.equal( - await client.clientGetName(), - 'name' - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - name: 'name' + it('redis://localhost', () => { + assert.deepEqual( + RedisClient.parseURL('redis://localhost'), + { + socket: { + host: 'localhost', + } } + ); }); + }); + + describe('authentication', () => { + testUtils.testWithClient('Client should be authenticated', async client => { + assert.equal( + await client.ping(), + 'PONG' + ); + }, GLOBAL.SERVERS.PASSWORD); + + testUtils.testWithClient('should execute AUTH before SELECT', async client => { + assert.equal( + (await client.clientInfo()).db, + 2 + ); + }, { + ...GLOBAL.SERVERS.PASSWORD, + clientOptions: { + ...GLOBAL.SERVERS.PASSWORD.clientOptions, + database: 2 + }, + minimumDockerVersion: [6, 2] + }); + }); - describe('legacyMode', () => { - testUtils.testWithClient('client.sendCommand should call the callback', async client => { - assert.equal( - await promisify(client.sendCommand).call(client, 'PING'), - 'PONG' - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.sendCommand should work without callback', async client => { - client.sendCommand(['PING']); - await client.v4.ping(); // make sure the first command was replied - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.sendCommand should reply with error', async client => { - await assert.rejects( - promisify(client.sendCommand).call(client, '1', '2') - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.hGetAll should reply with error', async client => { - await assert.rejects( - promisify(client.hGetAll).call(client) - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.v4.sendCommand should return a promise', async client => { - assert.equal( - await client.v4.sendCommand(['PING']), - 'PONG' - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.v4.{command} should return a promise', async client => { - assert.equal( - await client.v4.ping(), - 'PONG' - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.{command} should accept vardict arguments', async client => { - assert.equal( - await promisify(client.set).call(client, 'a', 'b'), - 'OK' - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.{command} should accept arguments array', async client => { - assert.equal( - await promisify(client.set).call(client, ['a', 'b']), - 'OK' - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.{command} should accept mix of arrays and arguments', async client => { - assert.equal( - await promisify(client.set).call(client, ['a'], 'b', ['EX', 1]), - 'OK' - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.hGetAll should return object', async client => { - await client.v4.hSet('key', 'field', 'value'); - - assert.deepEqual( - await promisify(client.hGetAll).call(client, 'key'), - Object.create(null, { - field: { - value: 'value', - configurable: true, - enumerable: true - } - }) - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); + testUtils.testWithClient('should set connection name', async client => { + assert.equal( + await client.clientGetName(), + 'name' + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + name: 'name' + } + }); + + // TODO: fix & uncomment + // testUtils.testWithClient('connect, ready and end events', async client => { + // await Promise.all([ + // once(client, 'connect'), + // once(client, 'ready'), + // client.connect() + // ]); + + // await Promise.all([ + // once(client, 'end'), + // client.close() + // ]); + // }, { + // ...GLOBAL.SERVERS.OPEN, + // disableClientSetup: true + // }); + + describe('sendCommand', () => { + testUtils.testWithClient('PING', async client => { + assert.equal(await client.sendCommand(['PING']), 'PONG'); + }, GLOBAL.SERVERS.OPEN); - function multiExecAsync< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >(multi: RedisClientMultiCommandType): Promise> { - return new Promise((resolve, reject) => { - (multi as any).exec((err: Error | undefined, replies: Array) => { - if (err) return reject(err); - - resolve(replies); - }); - }); + describe('AbortController', () => { + before(function () { + if (!global.AbortController) { + this.skip(); } + }); - testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => { - assert.deepEqual( - await multiExecAsync( - client.multi().ping() - ), - ['PONG'] - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } + testUtils.testWithClient('success', async client => { + await client.sendCommand(['PING'], { + abortSignal: new AbortController().signal }); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('client.multi.ping.exec should call the callback', async client => { - client.multi() - .ping() - .exec(); - await client.v4.ping(); // make sure the first command was replied - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); + testUtils.testWithClient('AbortError', client => { + const controller = new AbortController(); + controller.abort(); - testUtils.testWithClient('client.multi.ping.v4.ping.v4.exec should return a promise', async client => { - assert.deepEqual( - await client.multi() - .ping() - .v4.ping() - .v4.exec(), - ['PONG', 'PONG'] - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); + return assert.rejects( + client.sendCommand(['PING'], { + abortSignal: controller.signal + }), + AbortError + ); + }, GLOBAL.SERVERS.OPEN); + }); - testUtils.testWithClient('client.{script} should return a promise', async client => { - assert.equal( - await client.square(2), - 4 - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true, - scripts: { - square: SQUARE_SCRIPT - } - } - }); + testUtils.testWithClient('undefined and null should not break the client', async client => { + await assert.rejects( + client.sendCommand([null as any, undefined as any]), + TypeError + ); - testUtils.testWithClient('client.multi.{command}.exec should flatten array arguments', async client => { - assert.deepEqual( - await client.multi() - .sAdd('a', ['b', 'c']) - .v4.exec(), - [2] - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - - testUtils.testWithClient('client.multi.hGetAll should return object', async client => { - assert.deepEqual( - await multiExecAsync( - client.multi() - .hSet('key', 'field', 'value') - .hGetAll('key') - ), - [ - 1, - Object.create(null, { - field: { - value: 'value', - configurable: true, - enumerable: true - } - }) - ] - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - legacyMode: true - } - }); - }); + assert.equal( + await client.ping(), + 'PONG' + ); + }, GLOBAL.SERVERS.OPEN); + }); + + describe('multi', () => { + testUtils.testWithClient('simple', async client => { + assert.deepEqual( + await client.multi() + .ping() + .set('key', 'value') + .get('key') + .exec(), + ['PONG', 'OK', 'value'] + ); + }, GLOBAL.SERVERS.OPEN); - describe('events', () => { - testUtils.testWithClient('connect, ready, end', async client => { - await Promise.all([ - once(client, 'connect'), - once(client, 'ready'), - client.connect() - ]); - - await Promise.all([ - once(client, 'end'), - client.disconnect() - ]); - }, { - ...GLOBAL.SERVERS.OPEN, - disableClientSetup: true - }); - }); + testUtils.testWithClient('should reject the whole chain on error', client => { + return assert.rejects( + client.multi() + .ping() + .addCommand(['INVALID COMMAND']) + .ping() + .exec() + ); + }, GLOBAL.SERVERS.OPEN); - describe('sendCommand', () => { - testUtils.testWithClient('PING', async client => { - assert.equal(await client.sendCommand(['PING']), 'PONG'); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('returnBuffers', async client => { - assert.deepEqual( - await client.sendCommand(['PING'], { - returnBuffers: true - }), - Buffer.from('PONG') - ); - }, GLOBAL.SERVERS.OPEN); - - describe('AbortController', () => { - before(function () { - if (!global.AbortController) { - this.skip(); - } - }); - - testUtils.testWithClient('success', async client => { - await client.sendCommand(['PING'], { - signal: new AbortController().signal - }); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('AbortError', client => { - const controller = new AbortController(); - controller.abort(); - - return assert.rejects( - client.sendCommand(['PING'], { - signal: controller.signal - }), - AbortError - ); - }, GLOBAL.SERVERS.OPEN); - }); + testUtils.testWithClient('should reject the whole chain upon client disconnect', async client => { + await client.close(); + + return assert.rejects( + client.multi() + .ping() + .set('key', 'value') + .get('key') + .exec(), + ClientClosedError + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('undefined and null should not break the client', async client => { - await assert.rejects( - client.sendCommand([null as any, undefined as any]), - TypeError - ); - - assert.equal( - await client.ping(), - 'PONG' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('with script', async client => { + assert.deepEqual( + await client.multi() + .set('key', '2') + .square('key') + .exec(), + ['OK', 4] + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + scripts: { + square: SQUARE_SCRIPT + } + } }); - describe('multi', () => { - testUtils.testWithClient('simple', async client => { - assert.deepEqual( - await client.multi() - .ping() - .set('key', 'value') - .get('key') - .exec(), - ['PONG', 'OK', 'value'] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('should reject the whole chain on error', client => { - return assert.rejects( - client.multi() - .ping() - .addCommand(['INVALID COMMAND']) - .ping() - .exec() - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('should reject the whole chain upon client disconnect', async client => { - await client.disconnect(); - - return assert.rejects( - client.multi() - .ping() - .set('key', 'value') - .get('key') - .exec(), - ClientClosedError - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('with script', async client => { - assert.deepEqual( - await client.multi() - .square(2) - .exec(), - [4] - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - scripts: { - square: SQUARE_SCRIPT - } - } - }); + testUtils.testWithClient('WatchError', async client => { + await client.watch('key'); - testUtils.testWithClient('WatchError', async client => { - await client.watch('key'); - - await client.set( - RedisClient.commandOptions({ - isolated: true - }), - 'key', - '1' - ); - - await assert.rejects( - client.multi() - .decr('key') - .exec(), - WatchError - ); - }, GLOBAL.SERVERS.OPEN); - - describe('execAsPipeline', () => { - testUtils.testWithClient('exec(true)', async client => { - assert.deepEqual( - await client.multi() - .ping() - .exec(true), - ['PONG'] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('empty execAsPipeline', async client => { - assert.deepEqual( - await client.multi().execAsPipeline(), - [] - ); - }, GLOBAL.SERVERS.OPEN); - }); + const duplicate = await client.duplicate().connect(); + try { + await client.set( + 'key', + '1' + ); + } finally { + duplicate.destroy(); + } + + await assert.rejects( + client.multi() + .decr('key') + .exec(), + WatchError + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('should remember selected db', async client => { - await client.multi() - .select(1) - .exec(); - await killClient(client); - assert.equal( - (await client.clientInfo()).db, - 1 - ); - }, { - ...GLOBAL.SERVERS.OPEN, - minimumDockerVersion: [6, 2] // CLIENT INFO - }); + describe('execAsPipeline', () => { + testUtils.testWithClient('exec(true)', async client => { + assert.deepEqual( + await client.multi() + .ping() + .exec(true), + ['PONG'] + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('should handle error replies (#2665)', async client => { - await assert.rejects( - client.multi() - .set('key', 'value') - .hGetAll('key') - .exec(), - err => { - assert.ok(err instanceof MultiErrorReply); - assert.equal(err.replies.length, 2); - assert.deepEqual(err.errorIndexes, [1]); - assert.ok(err.replies[1] instanceof ErrorReply); - assert.deepEqual([...err.errors()], [err.replies[1]]); - return true; - } - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('empty execAsPipeline', async client => { + assert.deepEqual( + await client.multi().execAsPipeline(), + [] + ); + }, GLOBAL.SERVERS.OPEN); }); - testUtils.testWithClient('scripts', async client => { - assert.equal( - await client.square(2), - 4 - ); + testUtils.testWithClient('should remember selected db', async client => { + await client.multi() + .select(1) + .exec(); + await killClient(client); + assert.equal( + (await client.clientInfo()).db, + 1 + ); }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - scripts: { - square: SQUARE_SCRIPT - } - } + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [6, 2] // CLIENT INFO }); - const module = { - echo: { - transformArguments(message: string): Array { - return ['ECHO', message]; - }, - transformReply(reply: string): string { - return reply; - } + testUtils.testWithClient('should handle error replies (#2665)', async client => { + await assert.rejects( + client.multi() + .set('key', 'value') + .hGetAll('key') + .exec(), + err => { + assert.ok(err instanceof MultiErrorReply); + assert.equal(err.replies.length, 2); + assert.deepEqual(err.errorIndexes, [1]); + assert.ok(err.replies[1] instanceof ErrorReply); + assert.deepEqual([...err.errors()], [err.replies[1]]); + return true; } - }; + ); + }, GLOBAL.SERVERS.OPEN); + }); + + testUtils.testWithClient('scripts', async client => { + const [, reply] = await Promise.all([ + client.set('key', '2'), + client.square('key') + ]); + + assert.equal(reply, 4); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + scripts: { + square: SQUARE_SCRIPT + } + } + }); + + const module = { + echo: { + transformArguments(message: string) { + return ['ECHO', message]; + }, + transformReply: undefined as unknown as () => BlobStringReply + } + }; - testUtils.testWithClient('modules', async client => { - assert.equal( - await client.module.echo('message'), - 'message' - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - modules: { - module - } - } - }); + testUtils.testWithClient('modules', async client => { + assert.equal( + await client.module.echo('message'), + 'message' + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + modules: { + module + } + } + }); + + testUtils.testWithClient('functions', async client => { + const [,, reply] = await Promise.all([ + loadMathFunction(client), + client.set('key', '2'), + client.math.square('key') + ]); + + assert.equal(reply, 4); + }, { + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [7, 0], + clientOptions: { + functions: { + math: MATH_FUNCTION.library + } + } + }); - testUtils.testWithClient('functions', async client => { - await loadMathFunction(client); + testUtils.testWithClient('duplicate should reuse command options', async client => { + const duplicate = client.duplicate(); - assert.equal( - await client.math.square(2), - 4 - ); - }, { - ...GLOBAL.SERVERS.OPEN, - minimumDockerVersion: [7, 0], - clientOptions: { - functions: { - math: MATH_FUNCTION.library - } + await duplicate.connect(); + + try { + assert.deepEqual( + await duplicate.ping(), + Buffer.from('PONG') + ); + } finally { + duplicate.close(); + } + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + commandOptions: { + typeMapping: { + [RESP_TYPES.SIMPLE_STRING]: Buffer } - }); + } + }, + disableClientSetup: true, + }); + + async function killClient( + client: RedisClientType, + errorClient: RedisClientType = client + ): Promise { + const onceErrorPromise = once(errorClient, 'error'); + await client.sendCommand(['QUIT']); + await Promise.all([ + onceErrorPromise, + assert.rejects(client.ping()) + ]); + } + + testUtils.testWithClient('should reconnect when socket disconnects', async client => { + await killClient(client); + await assert.doesNotReject(client.ping()); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('should remember selected db', async client => { + await client.select(1); + await killClient(client); + assert.equal( + (await client.clientInfo()).db, + 1 + ); + }, { + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [6, 2] // CLIENT INFO + }); + + testUtils.testWithClient('scanIterator', async client => { + const entries: Array = [], + keys = new Set(); + for (let i = 0; i < 100; i++) { + const key = i.toString(); + keys.add(key); + entries.push(key, ''); + } - describe('isolationPool', () => { - testUtils.testWithClient('executeIsolated', async client => { - const id = await client.clientId(), - isolatedId = await client.executeIsolated(isolatedClient => isolatedClient.clientId()); - assert.ok(id !== isolatedId); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('should be able to use pool even before connect', async client => { - await client.executeIsolated(() => Promise.resolve()); - // make sure to destroy isolation pool - await client.connect(); - await client.disconnect(); - }, { - ...GLOBAL.SERVERS.OPEN, - disableClientSetup: true - }); + await client.mSet(entries); - testUtils.testWithClient('should work after reconnect (#2406)', async client => { - await client.disconnect(); - await client.connect(); - await client.executeIsolated(() => Promise.resolve()); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('should throw ClientClosedError after disconnect', async client => { - await client.connect(); - await client.disconnect(); - await assert.rejects( - client.executeIsolated(() => Promise.resolve()), - ClientClosedError - ); - }, { - ...GLOBAL.SERVERS.OPEN, - disableClientSetup: true - }); - }); + const results = new Set(); + for await (const keys of client.scanIterator()) { + for (const key of keys) { + results.add(key); + } + } - async function killClient< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >( - client: RedisClientType, - errorClient: RedisClientType = client - ): Promise { - const onceErrorPromise = once(errorClient, 'error'); - await client.sendCommand(['QUIT']); - await Promise.all([ - onceErrorPromise, - assert.rejects(client.ping(), SocketClosedUnexpectedlyError) - ]); + assert.deepEqual(keys, results); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('hScanIterator', async client => { + const hash: Record = {}; + for (let i = 0; i < 100; i++) { + hash[i.toString()] = i.toString(); } - testUtils.testWithClient('should reconnect when socket disconnects', async client => { - await killClient(client); - await assert.doesNotReject(client.ping()); - }, GLOBAL.SERVERS.OPEN); + await client.hSet('key', hash); - testUtils.testWithClient('should remember selected db', async client => { - await client.select(1); - await killClient(client); - assert.equal( - (await client.clientInfo()).db, - 1 - ); - }, { - ...GLOBAL.SERVERS.OPEN, - minimumDockerVersion: [6, 2] // CLIENT INFO - }); + const results: Record = {}; + for await (const entries of client.hScanIterator('key')) { + for (const { field, value } of entries) { + results[field] = value; + } + } - testUtils.testWithClient('should propagated errors from "isolated" clients', client => { - client.on('error', () => { - // ignore errors - }); - return client.executeIsolated(isolated => killClient(isolated, client)); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(hash, results); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('scanIterator', async client => { - const promises = [], - keys = new Set(); - for (let i = 0; i < 100; i++) { - const key = i.toString(); - keys.add(key); - promises.push(client.set(key, '')); - } + testUtils.testWithClient('hScanNoValuesIterator', async client => { + const hash: Record = {}; + const expectedFields: Array = []; + for (let i = 0; i < 100; i++) { + hash[i.toString()] = i.toString(); + expectedFields.push(i.toString()); + } - await Promise.all(promises); + await client.hSet('key', hash); - const results = new Set(); - for await (const key of client.scanIterator()) { - results.add(key); - } + const actualFields: Array = []; + for await (const fields of client.hScanNoValuesIterator('key')) { + for (const field of fields) { + actualFields.push(field); + } + } - assert.deepEqual(keys, results); - }, GLOBAL.SERVERS.OPEN); + function sort(a: string, b: string) { + return Number(a) - Number(b); + } - testUtils.testWithClient('hScanIterator', async client => { - const hash: Record = {}; - for (let i = 0; i < 100; i++) { - hash[i.toString()] = i.toString(); - } + assert.deepEqual(actualFields.sort(sort), expectedFields); + }, { + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [7, 4] + }); - await client.hSet('key', hash); + testUtils.testWithClient('sScanIterator', async client => { + const members = new Set(); + for (let i = 0; i < 100; i++) { + members.add(i.toString()); + } - const results: Record = {}; - for await (const { field, value } of client.hScanIterator('key')) { - results[field] = value; - } + await client.sAdd('key', Array.from(members)); - assert.deepEqual(hash, results); - }, GLOBAL.SERVERS.OPEN); + const results = new Set(); + for await (const members of client.sScanIterator('key')) { + for (const member of members) { + results.add(member); + } + } - testUtils.testWithClient('hScanNoValuesIterator', async client => { - const hash: Record = {}; - const expectedKeys: Array = []; - for (let i = 0; i < 100; i++) { - hash[i.toString()] = i.toString(); - expectedKeys.push(i.toString()); - } + assert.deepEqual(members, results); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('zScanIterator', async client => { + const members: Array = [], + map = new Map(); + for (let i = 0; i < 100; i++) { + const member = { + value: i.toString(), + score: 1 + }; + map.set(member.value, member.score); + members.push(member); + } - await client.hSet('key', hash); + await client.zAdd('key', members); - const keys: Array = []; - for await (const key of client.hScanNoValuesIterator('key')) { - keys.push(key); - } + const results = new Map(); + for await (const members of client.zScanIterator('key')) { + for (const { value, score } of members) { + results.set(value, score); + } + } - function sort(a: string, b: string) { - return Number(a) - Number(b); - } + assert.deepEqual(map, results); + }, GLOBAL.SERVERS.OPEN); - assert.deepEqual(keys.sort(sort), expectedKeys); - }, { - ...GLOBAL.SERVERS.OPEN, - minimumDockerVersion: [7, 4] - }); + describe('PubSub', () => { + testUtils.testWithClient('should be able to publish and subscribe to messages', async publisher => { + function assertStringListener(message: string, channel: string) { + assert.equal(typeof message, 'string'); + assert.equal(typeof channel, 'string'); + } - testUtils.testWithClient('sScanIterator', async client => { - const members = new Set(); - for (let i = 0; i < 100; i++) { - members.add(i.toString()); - } + function assertBufferListener(message: Buffer, channel: Buffer) { + assert.ok(message instanceof Buffer); + assert.ok(channel instanceof Buffer); + } - await client.sAdd('key', Array.from(members)); + const subscriber = await publisher.duplicate().connect(); - const results = new Set(); - for await (const key of client.sScanIterator('key')) { - results.add(key); - } + try { + const channelListener1 = spy(assertBufferListener), + channelListener2 = spy(assertStringListener), + patternListener = spy(assertStringListener); - assert.deepEqual(members, results); + await Promise.all([ + subscriber.subscribe('channel', channelListener1, true), + subscriber.subscribe('channel', channelListener2), + subscriber.pSubscribe('channel*', patternListener) + ]); + await Promise.all([ + waitTillBeenCalled(channelListener1), + waitTillBeenCalled(channelListener2), + waitTillBeenCalled(patternListener), + publisher.publish(Buffer.from('channel'), Buffer.from('message')) + ]); + assert.ok(channelListener1.calledOnceWithExactly(Buffer.from('message'), Buffer.from('channel'))); + assert.ok(channelListener2.calledOnceWithExactly('message', 'channel')); + assert.ok(patternListener.calledOnceWithExactly('message', 'channel')); + + await subscriber.unsubscribe('channel', channelListener1, true); + await Promise.all([ + waitTillBeenCalled(channelListener2), + waitTillBeenCalled(patternListener), + publisher.publish('channel', 'message') + ]); + assert.ok(channelListener1.calledOnce); + assert.ok(channelListener2.calledTwice); + assert.ok(channelListener2.secondCall.calledWithExactly('message', 'channel')); + assert.ok(patternListener.calledTwice); + assert.ok(patternListener.secondCall.calledWithExactly('message', 'channel')); + await subscriber.unsubscribe('channel'); + await Promise.all([ + waitTillBeenCalled(patternListener), + publisher.publish('channel', 'message') + ]); + assert.ok(channelListener1.calledOnce); + assert.ok(channelListener2.calledTwice); + assert.ok(patternListener.calledThrice); + assert.ok(patternListener.thirdCall.calledWithExactly('message', 'channel')); + + await subscriber.pUnsubscribe(); + await publisher.publish('channel', 'message'); + assert.ok(channelListener1.calledOnce); + assert.ok(channelListener2.calledTwice); + assert.ok(patternListener.calledThrice); + + // should be able to send commands when unsubsribed from all channels (see #1652) + await assert.doesNotReject(subscriber.ping()); + } finally { + subscriber.destroy(); + } }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('zScanIterator', async client => { - const members = []; - for (let i = 0; i < 100; i++) { - members.push({ - score: 1, - value: i.toString() - }); - } + testUtils.testWithClient('should resubscribe', async publisher => { + const subscriber = await publisher.duplicate().connect(); - await client.zAdd('key', members); + try { + const channelListener = spy(); + await subscriber.subscribe('channel', channelListener); - const map = new Map(); - for await (const member of client.zScanIterator('key')) { - map.set(member.value, member.score); - } + const patternListener = spy(); + await subscriber.pSubscribe('channe*', patternListener); - type MemberTuple = [string, number]; + await Promise.all([ + once(subscriber, 'error'), + publisher.clientKill({ + filter: 'SKIPME', + skipMe: true + }) + ]); - function sort(a: MemberTuple, b: MemberTuple) { - return Number(b[0]) - Number(a[0]); - } + await once(subscriber, 'ready'); - assert.deepEqual( - [...map.entries()].sort(sort), - members.map(member => [member.value, member.score]).sort(sort) - ); + await Promise.all([ + waitTillBeenCalled(channelListener), + waitTillBeenCalled(patternListener), + publisher.publish('channel', 'message') + ]); + } finally { + subscriber.destroy(); + } }, GLOBAL.SERVERS.OPEN); - - describe('PubSub', () => { - testUtils.testWithClient('should be able to publish and subscribe to messages', async publisher => { - function assertStringListener(message: string, channel: string) { - assert.equal(typeof message, 'string'); - assert.equal(typeof channel, 'string'); - } - - function assertBufferListener(message: Buffer, channel: Buffer) { - assert.ok(Buffer.isBuffer(message)); - assert.ok(Buffer.isBuffer(channel)); - } - - const subscriber = publisher.duplicate(); - - await subscriber.connect(); - - try { - const channelListener1 = spy(assertBufferListener), - channelListener2 = spy(assertStringListener), - patternListener = spy(assertStringListener); - - await Promise.all([ - subscriber.subscribe('channel', channelListener1, true), - subscriber.subscribe('channel', channelListener2), - subscriber.pSubscribe('channel*', patternListener) - ]); - await Promise.all([ - waitTillBeenCalled(channelListener1), - waitTillBeenCalled(channelListener2), - waitTillBeenCalled(patternListener), - publisher.publish(Buffer.from('channel'), Buffer.from('message')) - ]); - - assert.ok(channelListener1.calledOnceWithExactly(Buffer.from('message'), Buffer.from('channel'))); - assert.ok(channelListener2.calledOnceWithExactly('message', 'channel')); - assert.ok(patternListener.calledOnceWithExactly('message', 'channel')); - - await subscriber.unsubscribe('channel', channelListener1, true); - await Promise.all([ - waitTillBeenCalled(channelListener2), - waitTillBeenCalled(patternListener), - publisher.publish('channel', 'message') - ]); - assert.ok(channelListener1.calledOnce); - assert.ok(channelListener2.calledTwice); - assert.ok(channelListener2.secondCall.calledWithExactly('message', 'channel')); - assert.ok(patternListener.calledTwice); - assert.ok(patternListener.secondCall.calledWithExactly('message', 'channel')); - await subscriber.unsubscribe('channel'); - await Promise.all([ - waitTillBeenCalled(patternListener), - publisher.publish('channel', 'message') - ]); - assert.ok(channelListener1.calledOnce); - assert.ok(channelListener2.calledTwice); - assert.ok(patternListener.calledThrice); - assert.ok(patternListener.thirdCall.calledWithExactly('message', 'channel')); - await subscriber.pUnsubscribe(); - await publisher.publish('channel', 'message'); - assert.ok(channelListener1.calledOnce); - assert.ok(channelListener2.calledTwice); - assert.ok(patternListener.calledThrice); - // should be able to send commands when unsubsribed from all channels (see #1652) - await assert.doesNotReject(subscriber.ping()); - } finally { - await subscriber.disconnect(); - } - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('should resubscribe', async publisher => { - const subscriber = publisher.duplicate(); - - await subscriber.connect(); - - try { - const channelListener = spy(); - await subscriber.subscribe('channel', channelListener); - - const patternListener = spy(); - await subscriber.pSubscribe('channe*', patternListener); - - await Promise.all([ - once(subscriber, 'error'), - publisher.clientKill({ - filter: ClientKillFilters.SKIP_ME, - skipMe: true - }) - ]); - - await once(subscriber, 'ready'); - - await Promise.all([ - waitTillBeenCalled(channelListener), - waitTillBeenCalled(patternListener), - publisher.publish('channel', 'message') - ]); - } finally { - await subscriber.disconnect(); - } - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('should not fail when message arrives right after subscribe', async publisher => { - const subscriber = publisher.duplicate(); - - await subscriber.connect(); - - try { - await assert.doesNotReject(Promise.all([ - subscriber.subscribe('channel', () => { - // noop - }), - publisher.publish('channel', 'message') - ])); - } finally { - await subscriber.disconnect(); - } - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('should be able to quit in PubSub mode', async client => { - await client.subscribe('channel', () => { - // noop - }); - - await assert.doesNotReject(client.quit()); - - assert.equal(client.isOpen, false); - }, GLOBAL.SERVERS.OPEN); - }); - testUtils.testWithClient('ConnectionTimeoutError', async client => { - const promise = assert.rejects(client.connect(), ConnectionTimeoutError), - start = process.hrtime.bigint(); + testUtils.testWithClient('should not fail when message arrives right after subscribe', async publisher => { + const subscriber = await publisher.duplicate().connect(); + + try { + await assert.doesNotReject(Promise.all([ + subscriber.subscribe('channel', () => { + // noop + }), + publisher.publish('channel', 'message') + ])); + } finally { + subscriber.destroy(); + } + }, GLOBAL.SERVERS.OPEN); - while (process.hrtime.bigint() - start < 1_000_000) { - // block the event loop for 1ms, to make sure the connection will timeout - } + testUtils.testWithClient('should be able to quit in PubSub mode', async client => { + await client.subscribe('channel', () => { + // noop + }); - await promise; - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - socket: { - connectTimeout: 1 - } - }, - disableClientSetup: true - }); + await assert.doesNotReject(client.quit()); - testUtils.testWithClient('client.quit', async client => { - await client.connect(); + assert.equal(client.isOpen, false); + }, GLOBAL.SERVERS.OPEN); + }); - const pingPromise = client.ping(), - quitPromise = client.quit(); - assert.equal(client.isOpen, false); + testUtils.testWithClient('ConnectionTimeoutError', async client => { + const promise = assert.rejects(client.connect(), ConnectionTimeoutError), + start = process.hrtime.bigint(); - const [ping, quit] = await Promise.all([ - pingPromise, - quitPromise, - assert.rejects(client.ping(), ClientClosedError) - ]); + while (process.hrtime.bigint() - start < 1_000_000) { + // block the event loop for 1ms, to make sure the connection will timeout + } - assert.equal(ping, 'PONG'); - assert.equal(quit, 'OK'); - }, { - ...GLOBAL.SERVERS.OPEN, - disableClientSetup: true - }); + await promise; + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + socket: { + connectTimeout: 1 + } + }, + disableClientSetup: true + }); + + testUtils.testWithClient('client.quit', async client => { + await client.connect(); + + const pingPromise = client.ping(), + quitPromise = client.quit(); + assert.equal(client.isOpen, false); + + const [ping, quit] = await Promise.all([ + pingPromise, + quitPromise, + assert.rejects(client.ping(), ClientClosedError) + ]); + + assert.equal(ping, 'PONG'); + assert.equal(quit, 'OK'); + }, { + ...GLOBAL.SERVERS.OPEN, + disableClientSetup: true + }); + + testUtils.testWithClient('client.disconnect', async client => { + const pingPromise = client.ping(), + disconnectPromise = client.disconnect(); + assert.equal(client.isOpen, false); + await Promise.all([ + assert.rejects(pingPromise, DisconnectsClientError), + assert.doesNotReject(disconnectPromise), + assert.rejects(client.ping(), ClientClosedError) + ]); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('should be able to connect after disconnect (see #1801)', async client => { + await client.disconnect(); + await client.connect(); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('should be able to use ref and unref', client => { + client.unref(); + client.ref(); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('pingInterval', async client => { + assert.deepEqual( + await once(client, 'ping-interval'), + ['PONG'] + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + pingInterval: 1 + } + }); - testUtils.testWithClient('client.disconnect', async client => { - const pingPromise = client.ping(), - disconnectPromise = client.disconnect(); - assert.equal(client.isOpen, false); + testUtils.testWithClient('should reject commands in connect phase when `disableOfflineQueue`', async client => { + const connectPromise = client.connect(); + await assert.rejects( + client.ping(), + ClientOfflineError + ); + await connectPromise; + await client.disconnect(); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + disableOfflineQueue: true + }, + disableClientSetup: true + }); + + describe('MONITOR', () => { + testUtils.testWithClient('should be able to monitor commands', async client => { + const duplicate = await client.duplicate().connect(), + listener = spy(message => assert.equal(typeof message, 'string')); + await duplicate.monitor(listener); + + try { await Promise.all([ - assert.rejects(pingPromise, DisconnectsClientError), - assert.doesNotReject(disconnectPromise), - assert.rejects(client.ping(), ClientClosedError) + waitTillBeenCalled(listener), + client.ping() ]); + } finally { + duplicate.destroy(); + } }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('should be able to connect after disconnect (see #1801)', async client => { - await client.disconnect(); - await client.connect(); + testUtils.testWithClient('should keep monitoring after reconnection', async client => { + const duplicate = await client.duplicate().connect(), + listener = spy(message => assert.equal(typeof message, 'string')); + await duplicate.monitor(listener); + + try { + await Promise.all([ + once(duplicate, 'error'), + client.clientKill({ + filter: 'SKIPME', + skipMe: true + }) + ]); + + await once(duplicate, 'ready'); + + await Promise.all([ + waitTillBeenCalled(listener), + client.ping() + ]); + } finally { + duplicate.destroy(); + } }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('should be able to use ref and unref', client => { - client.unref(); - client.ref(); + testUtils.testWithClient('should be able to go back to "normal mode"', async client => { + await Promise.all([ + client.monitor(() => {}), + client.reset() + ]); + await assert.doesNotReject(client.ping()); }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('pingInterval', async client => { - assert.deepEqual( - await once(client, 'ping-interval'), - ['PONG'] - ); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - pingInterval: 1 - } - }); + testUtils.testWithClient('should respect type mapping', async client => { + const duplicate = await client.duplicate().connect(), + listener = spy(message => assert.ok(message instanceof Buffer)); + await duplicate.withTypeMapping({ + [RESP_TYPES.SIMPLE_STRING]: Buffer + }).monitor(listener); - testUtils.testWithClient('should reject commands in connect phase when `disableOfflineQueue`', async client => { - const connectPromise = client.connect(); - await assert.rejects( - client.ping(), - ClientOfflineError - ); - await connectPromise; - await client.disconnect(); - }, { - ...GLOBAL.SERVERS.OPEN, - clientOptions: { - disableOfflineQueue: true - }, - disableClientSetup: true - }); + try { + await Promise.all([ + waitTillBeenCalled(listener), + client.ping() + ]); + } finally { + duplicate.destroy(); + } + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index d7f33e97b16..64a3b578815 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -1,883 +1,1095 @@ -import COMMANDS from './commands'; -import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, ConvertArgumentType, RedisFunction, ExcludeMappedString, RedisCommands } from '../commands'; -import RedisSocket, { RedisSocketOptions, RedisTlsSocketOptions } from './socket'; -import RedisCommandsQueue, { QueueCommandOptions } from './commands-queue'; +import COMMANDS from '../commands'; +import RedisSocket, { RedisSocketOptions } from './socket'; +import RedisCommandsQueue, { CommandOptions } from './commands-queue'; +import { EventEmitter } from 'node:events'; +import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; +import { ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchError } from '../errors'; +import { URL } from 'node:url'; +import { TcpSocketConnectOpts } from 'node:net'; +import { PUBSUB_TYPE, PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub'; +import { Command, CommandSignature, TypeMapping, CommanderConfig, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, RedisArgument, ReplyWithTypeMapping, SimpleStringReply } from '../RESP/types'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; import { RedisMultiQueuedCommand } from '../multi-command'; -import { EventEmitter } from 'events'; -import { CommandOptions, commandOptions, isCommandOptions } from '../command-options'; -import { ScanOptions, ZMember } from '../commands/generic-transformers'; -import { ScanCommandOptions } from '../commands/SCAN'; -import { HScanTuple } from '../commands/HSCAN'; -import { attachCommands, attachExtensions, fCallArguments, transformCommandArguments, transformCommandReply, transformLegacyCommandArguments } from '../commander'; -import { Pool, Options as PoolOptions, createPool } from 'generic-pool'; -import { ClientClosedError, ClientOfflineError, DisconnectsClientError, ErrorReply } from '../errors'; -import { URL } from 'url'; -import { TcpSocketConnectOpts } from 'net'; -import { PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub'; - -import {version} from '../../package.json'; +import HELLO, { HelloOptions } from '../commands/HELLO'; +import { ScanOptions, ScanCommonOptions } from '../commands/SCAN'; +import { RedisLegacyClient, RedisLegacyClientType } from './legacy-mode'; +import { RedisPoolOptions, RedisClientPool } from './pool'; +import { RedisVariadicArgument, pushVariadicArguments } from '../commands/generic-transformers'; export interface RedisClientOptions< - M extends RedisModules = RedisModules, - F extends RedisFunctions = RedisFunctions, - S extends RedisScripts = RedisScripts -> extends RedisExtensions { - /** - * `redis[s]://[[username][:password]@][host][:port][/db-number]` - * See [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details - */ - url?: string; - /** - * Socket connection properties - */ - socket?: RedisSocketOptions; - /** - * ACL username ([see ACL guide](https://redis.io/topics/acl)) - */ - username?: string; - /** - * ACL password or the old "--requirepass" password - */ - password?: string; - /** - * Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) - */ - name?: string; - /** - * Redis database number (see [`SELECT`](https://redis.io/commands/select) command) - */ - database?: number; - /** - * Maximum length of the client's internal command queue - */ - commandsQueueMaxLength?: number; - /** - * When `true`, commands are rejected when the client is reconnecting. - * When `false`, commands are queued for execution after reconnection. - */ - disableOfflineQueue?: boolean; - /** - * Connect in [`READONLY`](https://redis.io/commands/readonly) mode - */ - readonly?: boolean; - legacyMode?: boolean; - isolationPoolOptions?: PoolOptions; - /** - * Send `PING` command at interval (in ms). - * Useful with Redis deployments that do not use TCP Keep-Alive. - */ - pingInterval?: number; - /** - * If set to true, disables sending client identifier (user-agent like message) to the redis server - */ - disableClientInfo?: boolean; - /** - * Tag to append to library name that is sent to the Redis server - */ - clientInfoTag?: string; + M extends RedisModules = RedisModules, + F extends RedisFunctions = RedisFunctions, + S extends RedisScripts = RedisScripts, + RESP extends RespVersions = RespVersions, + TYPE_MAPPING extends TypeMapping = TypeMapping, + SocketOptions extends RedisSocketOptions = RedisSocketOptions +> extends CommanderConfig { + /** + * `redis[s]://[[username][:password]@][host][:port][/db-number]` + * See [`redis`](https://www.iana.org/assignments/uri-schemes/prov/redis) and [`rediss`](https://www.iana.org/assignments/uri-schemes/prov/rediss) IANA registration for more details + */ + url?: string; + /** + * Socket connection properties + */ + socket?: SocketOptions; + /** + * ACL username ([see ACL guide](https://redis.io/topics/acl)) + */ + username?: string; + /** + * ACL password or the old "--requirepass" password + */ + password?: string; + /** + * Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) + */ + name?: string; + /** + * Redis database number (see [`SELECT`](https://redis.io/commands/select) command) + */ + database?: number; + /** + * Maximum length of the client's internal command queue + */ + commandsQueueMaxLength?: number; + /** + * When `true`, commands are rejected when the client is reconnecting. + * When `false`, commands are queued for execution after reconnection. + */ + disableOfflineQueue?: boolean; + /** + * Connect in [`READONLY`](https://redis.io/commands/readonly) mode + */ + readonly?: boolean; + /** + * Send `PING` command at interval (in ms). + * Useful with Redis deployments that do not honor TCP Keep-Alive. + */ + pingInterval?: number; + /** + * TODO + */ + commandOptions?: CommandOptions; } -type WithCommands = { - [P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>; +type WithCommands< + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>; }; -export type WithModules = { - [P in keyof M as ExcludeMappedString

]: { - [C in keyof M[P] as ExcludeMappedString]: RedisCommandSignature; - }; +type WithModules< + M extends RedisModules, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof M]: { + [C in keyof M[P]]: CommandSignature; + }; }; -export type WithFunctions = { - [P in keyof F as ExcludeMappedString

]: { - [FF in keyof F[P] as ExcludeMappedString]: RedisCommandSignature; - }; +type WithFunctions< + F extends RedisFunctions, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [L in keyof F]: { + [C in keyof F[L]]: CommandSignature; + }; }; -export type WithScripts = { - [P in keyof S as ExcludeMappedString

]: RedisCommandSignature; +type WithScripts< + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof S]: CommandSignature; }; +export type RedisClientExtensions< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} +> = ( + WithCommands & + WithModules & + WithFunctions & + WithScripts +); + export type RedisClientType< - M extends RedisModules = Record, - F extends RedisFunctions = Record, - S extends RedisScripts = Record -> = RedisClient & WithCommands & WithModules & WithFunctions & WithScripts; - -export type InstantiableRedisClient< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = new (options?: RedisClientOptions) => RedisClientType; - -export interface ClientCommandOptions extends QueueCommandOptions { - isolated?: boolean; + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} +> = ( + RedisClient & + RedisClientExtensions +); + +type ProxyClient = RedisClient; + +type NamespaceProxyClient = { _self: ProxyClient }; + +interface ScanIteratorOptions { + cursor?: RedisArgument; } -type ClientLegacyCallback = (err: Error | null, reply?: RedisCommandRawReply) => void; +export type MonitorCallback = (reply: ReplyWithTypeMapping) => unknown; export default class RedisClient< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > extends EventEmitter { - static commandOptions(options: T): CommandOptions { - return commandOptions(options); - } - - commandOptions = RedisClient.commandOptions; - - static extend< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >(extensions?: RedisExtensions): InstantiableRedisClient { - const Client = attachExtensions({ - BaseClass: RedisClient, - modulesExecutor: RedisClient.prototype.commandsExecutor, - modules: extensions?.modules, - functionsExecutor: RedisClient.prototype.functionsExecuter, - functions: extensions?.functions, - scriptsExecutor: RedisClient.prototype.scriptsExecuter, - scripts: extensions?.scripts - }); - - if (Client !== RedisClient) { - Client.prototype.Multi = RedisClientMultiCommand.extend(extensions); - } - - return Client; - } - - static create< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >(options?: RedisClientOptions): RedisClientType { - return new (RedisClient.extend(options))(options); - } + static #createCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return async function (this: ProxyClient, ...args: Array) { + const redisArgs = command.transformArguments(...args); + const typeMapping = this._commandOptions?.typeMapping; - static parseURL(url: string): RedisClientOptions { - // https://www.iana.org/assignments/uri-schemes/prov/redis - const { hostname, port, protocol, username, password, pathname } = new URL(url), - parsed: RedisClientOptions = { - socket: { - host: hostname - } - }; - - if (protocol === 'rediss:') { - (parsed.socket as RedisTlsSocketOptions).tls = true; - } else if (protocol !== 'redis:') { - throw new TypeError('Invalid protocol'); - } + const reply = await this.sendCommand(redisArgs, this._commandOptions); - if (port) { - (parsed.socket as TcpSocketConnectOpts).port = Number(port); - } + return transformReply ? + transformReply(reply, redisArgs.preserve, typeMapping) : + reply; + }; + } - if (username) { - parsed.username = decodeURIComponent(username); - } + static #createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return async function (this: NamespaceProxyClient, ...args: Array) { + const redisArgs = command.transformArguments(...args); + const typeMapping = this._self._commandOptions?.typeMapping - if (password) { - parsed.password = decodeURIComponent(password); - } + const reply = await this._self.sendCommand(redisArgs, this._self._commandOptions); - if (pathname.length > 1) { - const database = Number(pathname.substring(1)); - if (isNaN(database)) { - throw new TypeError('Invalid pathname'); - } + return transformReply ? + transformReply(reply, redisArgs.preserve, typeMapping) : + reply; + }; + } + + static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn), + transformReply = getTransformReply(fn, resp); + return async function (this: NamespaceProxyClient, ...args: Array) { + const fnArgs = fn.transformArguments(...args); + const typeMapping = this._self._commandOptions?.typeMapping; + + const reply = await this._self.sendCommand( + prefix.concat(fnArgs), + this._self._commandOptions + ); - parsed.database = database; + return transformReply ? + transformReply(reply, fnArgs.preserve, typeMapping) : + reply; + }; + } + + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const prefix = scriptArgumentsPrefix(script), + transformReply = getTransformReply(script, resp); + return async function (this: ProxyClient, ...args: Array) { + const scriptArgs = script.transformArguments(...args); + const redisArgs = prefix.concat(scriptArgs); + const typeMapping = this._commandOptions?.typeMapping; + + const reply = await this.executeScript(script, redisArgs, this._commandOptions); + + return transformReply ? + transformReply(reply, scriptArgs.preserve, typeMapping) : + reply; + }; + } + + static factory< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2 + >(config?: CommanderConfig) { + const Client = attachConfig({ + BaseClass: RedisClient, + commands: COMMANDS, + createCommand: RedisClient.#createCommand, + createModuleCommand: RedisClient.#createModuleCommand, + createFunctionCommand: RedisClient.#createFunctionCommand, + createScriptCommand: RedisClient.#createScriptCommand, + config + }); + + Client.prototype.Multi = RedisClientMultiCommand.extend(config); + + return ( + options?: Omit, keyof Exclude> + ) => { + // returning a "proxy" to prevent the namespaces._self to leak between "proxies" + return Object.create(new Client(options)) as RedisClientType; + }; + } + + static create< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + >(this: void, options?: RedisClientOptions) { + return RedisClient.factory(options)(options); + } + + static parseURL(url: string): RedisClientOptions { + // https://www.iana.org/assignments/uri-schemes/prov/redis + const { hostname, port, protocol, username, password, pathname } = new URL(url), + parsed: RedisClientOptions = { + socket: { + host: hostname } + }; - return parsed; + if (protocol === 'rediss:') { + parsed!.socket!.tls = true; + } else if (protocol !== 'redis:') { + throw new TypeError('Invalid protocol'); } - readonly #options?: RedisClientOptions; - readonly #socket: RedisSocket; - readonly #queue: RedisCommandsQueue; - #isolationPool?: Pool>; - readonly #v4: Record = {}; - #selectedDB = 0; - - get options(): RedisClientOptions | undefined { - return this.#options; + if (port) { + (parsed.socket as TcpSocketConnectOpts).port = Number(port); } - get isOpen(): boolean { - return this.#socket.isOpen; + if (username) { + parsed.username = decodeURIComponent(username); } - get isReady(): boolean { - return this.#socket.isReady; + if (password) { + parsed.password = decodeURIComponent(password); } - get isPubSubActive() { - return this.#queue.isPubSubActive; - } + if (pathname.length > 1) { + const database = Number(pathname.substring(1)); + if (isNaN(database)) { + throw new TypeError('Invalid pathname'); + } - get v4(): Record { - if (!this.#options?.legacyMode) { - throw new Error('the client is not in "legacy mode"'); - } - - return this.#v4; + parsed.database = database; } - constructor(options?: RedisClientOptions) { - super(); - this.#options = this.#initiateOptions(options); - this.#queue = this.#initiateQueue(); - this.#socket = this.#initiateSocket(); - // should be initiated in connect, not here - // TODO: consider breaking in v5 - this.#isolationPool = this.#initiateIsolationPool(); - this.#legacyMode(); + return parsed; + } + + readonly #options?: RedisClientOptions; + readonly #socket: RedisSocket; + readonly #queue: RedisCommandsQueue; + #selectedDB = 0; + #monitorCallback?: MonitorCallback; + private _self = this; + private _commandOptions?: CommandOptions; + #dirtyWatch?: string; + #epoch: number; + #watchEpoch?: number; + + get options(): RedisClientOptions | undefined { + return this._self.#options; + } + + get isOpen(): boolean { + return this._self.#socket.isOpen; + } + + get isReady(): boolean { + return this._self.#socket.isReady; + } + + get isPubSubActive() { + return this._self.#queue.isPubSubActive; + } + + get isWatching() { + return this._self.#watchEpoch !== undefined; + } + + setDirtyWatch(msg: string) { + this._self.#dirtyWatch = msg; + } + + constructor(options?: RedisClientOptions) { + super(); + this.#options = this.#initiateOptions(options); + this.#queue = this.#initiateQueue(); + this.#socket = this.#initiateSocket(); + this.#epoch = 0; + } + + #initiateOptions(options?: RedisClientOptions): RedisClientOptions | undefined { + if (options?.url) { + const parsed = RedisClient.parseURL(options.url); + if (options.socket) { + parsed.socket = Object.assign(options.socket, parsed.socket); + } + + Object.assign(options, parsed); } - #initiateOptions(options?: RedisClientOptions): RedisClientOptions | undefined { - if (options?.url) { - const parsed = RedisClient.parseURL(options.url); - if (options.socket) { - parsed.socket = Object.assign(options.socket, parsed.socket); - } - - Object.assign(options, parsed); - } - - if (options?.database) { - this.#selectedDB = options.database; - } - - return options; + if (options?.database) { + this._self.#selectedDB = options.database; } - #initiateQueue(): RedisCommandsQueue { - return new RedisCommandsQueue( - this.#options?.commandsQueueMaxLength, - (channel, listeners) => this.emit('sharded-channel-moved', channel, listeners) - ); + if (options?.commandOptions) { + this._commandOptions = options.commandOptions; } - #initiateSocket(): RedisSocket { - const socketInitiator = async (): Promise => { - const promises = []; - - if (this.#selectedDB !== 0) { - promises.push( - this.#queue.addCommand( - ['SELECT', this.#selectedDB.toString()], - { asap: true } - ) - ); - } + return options; + } - if (this.#options?.readonly) { - promises.push( - this.#queue.addCommand( - COMMANDS.READONLY.transformArguments(), - { asap: true } - ) - ); - } + #initiateQueue(): RedisCommandsQueue { + return new RedisCommandsQueue( + this.#options?.RESP ?? 2, + this.#options?.commandsQueueMaxLength, + (channel, listeners) => this.emit('sharded-channel-moved', channel, listeners) + ); + } - if (!this.#options?.disableClientInfo) { - promises.push( - this.#queue.addCommand( - [ 'CLIENT', 'SETINFO', 'LIB-VER', version], - { asap: true } - ).catch(err => { - if (!(err instanceof ErrorReply)) { - throw err; - } - }) - ); - - promises.push( - this.#queue.addCommand( - [ - 'CLIENT', 'SETINFO', 'LIB-NAME', - this.#options?.clientInfoTag ? `node-redis(${this.#options.clientInfoTag})` : 'node-redis' - ], - { asap: true } - ).catch(err => { - if (!(err instanceof ErrorReply)) { - throw err; - } - }) - ); - } - - if (this.#options?.name) { - promises.push( - this.#queue.addCommand( - COMMANDS.CLIENT_SETNAME.transformArguments(this.#options.name), - { asap: true } - ) - ); - } + #handshake(selectedDB: number) { + const commands = []; - if (this.#options?.username || this.#options?.password) { - promises.push( - this.#queue.addCommand( - COMMANDS.AUTH.transformArguments({ - username: this.#options.username, - password: this.#options.password ?? '' - }), - { asap: true } - ) - ); - } - - const resubscribePromise = this.#queue.resubscribe(); - if (resubscribePromise) { - promises.push(resubscribePromise); - } - - if (promises.length) { - this.#tick(true); - await Promise.all(promises); - } - }; - - return new RedisSocket(socketInitiator, this.#options?.socket) - .on('data', chunk => this.#queue.onReplyChunk(chunk)) - .on('error', err => { - this.emit('error', err); - if (this.#socket.isOpen && !this.#options?.disableOfflineQueue) { - this.#queue.flushWaitingForReply(err); - } else { - this.#queue.flushAll(err); - } - }) - .on('connect', () => { - this.emit('connect'); - }) - .on('ready', () => { - this.emit('ready'); - this.#setPingTimer(); - this.#tick(); - }) - .on('reconnecting', () => this.emit('reconnecting')) - .on('drain', () => this.#tick()) - .on('end', () => this.emit('end')); - } - - #initiateIsolationPool() { - return createPool({ - create: async () => { - const duplicate = this.duplicate({ - isolationPoolOptions: undefined - }).on('error', err => this.emit('error', err)); - await duplicate.connect(); - return duplicate; - }, - destroy: client => client.disconnect() - }, this.#options?.isolationPoolOptions); - } - - #legacyMode(): void { - if (!this.#options?.legacyMode) return; - - (this as any).#v4.sendCommand = this.#sendCommand.bind(this); - (this as any).sendCommand = (...args: Array): void => { - const result = this.#legacySendCommand(...args); - if (result) { - result.promise - .then(reply => result.callback(null, reply)) - .catch(err => result.callback(err)); - } - }; - - for (const [ name, command ] of Object.entries(COMMANDS as RedisCommands)) { - this.#defineLegacyCommand(name, command); - (this as any)[name.toLowerCase()] ??= (this as any)[name]; - } - - // 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'); - } - - #legacySendCommand(...args: Array) { - const callback = typeof args[args.length - 1] === 'function' ? - args.pop() as ClientLegacyCallback : - undefined; + if (this.#options?.RESP) { + const hello: HelloOptions = {}; - const promise = this.#sendCommand(transformLegacyCommandArguments(args)); - if (callback) return { - promise, - callback + if (this.#options.password) { + hello.AUTH = { + username: this.#options.username ?? 'default', + password: this.#options.password }; - promise.catch(err => this.emit('error', err)); - } - - #defineLegacyCommand(name: string, command?: RedisCommand): void { - this.#v4[name] = (this as any)[name].bind(this); - (this as any)[name] = command && command.TRANSFORM_LEGACY_REPLY && command.transformReply ? - (...args: Array) => { - const result = this.#legacySendCommand(name, ...args); - if (result) { - result.promise - .then(reply => result.callback(null, command.transformReply!(reply))) - .catch(err => result.callback(err)); - } - } : - (...args: Array) => (this as any).sendCommand(name, ...args); - } - - #pingTimer?: NodeJS.Timeout; - - #setPingTimer(): void { - if (!this.#options?.pingInterval || !this.#socket.isReady) return; - clearTimeout(this.#pingTimer); - - this.#pingTimer = setTimeout(() => { - if (!this.#socket.isReady) return; - - // using #sendCommand to support legacy mode - this.#sendCommand(['PING']) - .then(reply => this.emit('ping-interval', reply)) - .catch(err => this.emit('error', err)) - .finally(() => this.#setPingTimer()); - }, this.#options.pingInterval); - } - - duplicate(overrides?: Partial>): RedisClientType { - return new (Object.getPrototypeOf(this).constructor)({ - ...this.#options, - ...overrides - }); - } - - async connect() { - // see comment in constructor - this.#isolationPool ??= this.#initiateIsolationPool(); - await this.#socket.connect(); - return this as unknown as RedisClientType; - } + } + + if (this.#options.name) { + hello.SETNAME = this.#options.name; + } + + commands.push( + HELLO.transformArguments(this.#options.RESP, hello) + ); + } else { + if (this.#options?.username || this.#options?.password) { + commands.push( + COMMANDS.AUTH.transformArguments({ + username: this.#options.username, + password: this.#options.password ?? '' + }) + ); + } - async commandsExecutor( - command: C, - args: Array - ): Promise> { - const { args: redisArgs, options } = transformCommandArguments(command, args); - return transformCommandReply( - command, - await this.#sendCommand(redisArgs, options), - redisArgs.preserve + if (this.#options?.name) { + commands.push( + COMMANDS.CLIENT_SETNAME.transformArguments(this.#options.name) ); + } } - sendCommand( - args: RedisCommandArguments, - options?: ClientCommandOptions - ): Promise { - return this.#sendCommand(args, options); + if (selectedDB !== 0) { + commands.push(['SELECT', this.#selectedDB.toString()]); } - // using `#sendCommand` cause `sendCommand` is overwritten in legacy mode - #sendCommand( - args: RedisCommandArguments, - options?: ClientCommandOptions - ): Promise { - if (!this.#socket.isOpen) { - return Promise.reject(new ClientClosedError()); - } else if (options?.isolated) { - return this.executeIsolated(isolatedClient => - isolatedClient.sendCommand(args, { - ...options, - isolated: false - }) - ); - } else if (!this.#socket.isReady && this.#options?.disableOfflineQueue) { - return Promise.reject(new ClientOfflineError()); - } - - const promise = this.#queue.addCommand(args, options); - this.#tick(); - return promise; + if (this.#options?.readonly) { + commands.push( + COMMANDS.READONLY.transformArguments() + ); } - async functionsExecuter( - fn: F, - args: Array, - name: string - ): Promise> { - const { args: redisArgs, options } = transformCommandArguments(fn, args); - return transformCommandReply( - fn, - await this.executeFunction(name, fn, redisArgs, options), - redisArgs.preserve + return commands; + } + + #initiateSocket(): RedisSocket { + const socketInitiator = () => { + const promises = [], + chainId = Symbol('Socket Initiator'); + + const resubscribePromise = this.#queue.resubscribe(chainId); + if (resubscribePromise) { + promises.push(resubscribePromise); + } + + if (this.#monitorCallback) { + promises.push( + this.#queue.monitor( + this.#monitorCallback, + { + typeMapping: this._commandOptions?.typeMapping, + chainId, + asap: true + } + ) ); - } - - executeFunction( - name: string, - fn: RedisFunction, - args: RedisCommandArguments, - options?: ClientCommandOptions - ): Promise { - return this.#sendCommand( - fCallArguments(name, fn, args), - options + } + + const commands = this.#handshake(this.#selectedDB); + for (let i = commands.length - 1; i >= 0; --i) { + promises.push( + this.#queue.addCommand(commands[i], { + chainId, + asap: true + }) ); - } + } - async scriptsExecuter( - script: S, - args: Array - ): Promise> { - const { args: redisArgs, options } = transformCommandArguments(script, args); - return transformCommandReply( - script, - await this.executeScript(script, redisArgs, options), - redisArgs.preserve - ); - } - - async executeScript( - script: RedisScript, - args: RedisCommandArguments, - options?: ClientCommandOptions - ): Promise { - const redisArgs: RedisCommandArguments = ['EVALSHA', script.SHA1]; - - if (script.NUMBER_OF_KEYS !== undefined) { - redisArgs.push(script.NUMBER_OF_KEYS.toString()); - } - - redisArgs.push(...args); + if (promises.length) { + this.#write(); + return Promise.all(promises); + } + }; + return new RedisSocket(socketInitiator, this.#options?.socket) + .on('data', chunk => { try { - return await this.#sendCommand(redisArgs, options); - } catch (err: any) { - if (!err?.message?.startsWith?.('NOSCRIPT')) { - throw err; - } - - redisArgs[0] = 'EVAL'; - redisArgs[1] = script.SCRIPT; - return this.#sendCommand(redisArgs, options); + this.#queue.decoder.write(chunk); + } catch (err) { + this.#queue.resetDecoder(); + this.emit('error', err); } - } - - 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; + }) + .on('error', err => { + this.emit('error', err); + if (this.#socket.isOpen && !this.#options?.disableOfflineQueue) { + this.#queue.flushWaitingForReply(err); + } else { + this.#queue.flushAll(err); } - - await this.#sendCommand(['SELECT', db.toString()], options); - this.#selectedDB = db; - } - - select = this.SELECT; - - #pubSubCommand(promise: Promise | undefined) { - if (promise === undefined) return Promise.resolve(); - - this.#tick(); - return promise; - } - - SUBSCRIBE( - channels: string | Array, - listener: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.subscribe( - PubSubType.CHANNELS, - channels, - listener, - bufferMode - ) - ); - } - - subscribe = this.SUBSCRIBE; - - - UNSUBSCRIBE( - channels?: string | Array, - listener?: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.unsubscribe( - PubSubType.CHANNELS, - channels, - listener, - bufferMode - ) - ); - } - - unsubscribe = this.UNSUBSCRIBE; - - PSUBSCRIBE( - patterns: string | Array, - listener: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.subscribe( - PubSubType.PATTERNS, - patterns, - listener, - bufferMode - ) - ); - } - - pSubscribe = this.PSUBSCRIBE; - - PUNSUBSCRIBE( - patterns?: string | Array, - listener?: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.unsubscribe( - PubSubType.PATTERNS, - patterns, - listener, - bufferMode - ) - ); - } - - pUnsubscribe = this.PUNSUBSCRIBE; - - SSUBSCRIBE( - channels: string | Array, - listener: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.subscribe( - PubSubType.SHARDED, - channels, - listener, - bufferMode - ) - ); + }) + .on('connect', () => this.emit('connect')) + .on('ready', () => { + this.#epoch++; + this.emit('ready'); + this.#setPingTimer(); + this.#maybeScheduleWrite(); + }) + .on('reconnecting', () => this.emit('reconnecting')) + .on('drain', () => this.#maybeScheduleWrite()) + .on('end', () => this.emit('end')); + } + + #pingTimer?: NodeJS.Timeout; + + #setPingTimer(): void { + if (!this.#options?.pingInterval || !this.#socket.isReady) return; + clearTimeout(this.#pingTimer); + + this.#pingTimer = setTimeout(() => { + if (!this.#socket.isReady) return; + + this.sendCommand(['PING']) + .then(reply => this.emit('ping-interval', reply)) + .catch(err => this.emit('error', err)) + .finally(() => this.#setPingTimer()); + }, this.#options.pingInterval); + } + + withCommandOptions< + OPTIONS extends CommandOptions, + TYPE_MAPPING extends TypeMapping + >(options: OPTIONS) { + const proxy = Object.create(this._self); + proxy._commandOptions = options; + return proxy as RedisClientType< + M, + F, + S, + RESP, + TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {} + >; + } + + private _commandOptionsProxy< + K extends keyof CommandOptions, + V extends CommandOptions[K] + >( + key: K, + value: V + ) { + const proxy = Object.create(this._self); + proxy._commandOptions = Object.create(this._commandOptions ?? null); + proxy._commandOptions[key] = value; + return proxy as RedisClientType< + M, + F, + S, + RESP, + K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING + >; + } + + /** + * Override the `typeMapping` command option + */ + withTypeMapping(typeMapping: TYPE_MAPPING) { + return this._commandOptionsProxy('typeMapping', typeMapping); + } + + /** + * Override the `abortSignal` command option + */ + withAbortSignal(abortSignal: AbortSignal) { + return this._commandOptionsProxy('abortSignal', abortSignal); + } + + /** + * Override the `asap` command option to `true` + */ + asap() { + return this._commandOptionsProxy('asap', true); + } + + /** + * Create the "legacy" (v3/callback) interface + */ + legacy(): RedisLegacyClientType { + return new RedisLegacyClient( + this as unknown as RedisClientType + ) as RedisLegacyClientType; + } + + /** + * Create {@link RedisClientPool `RedisClientPool`} using this client as a prototype + */ + createPool(options?: Partial) { + return RedisClientPool.create( + this._self.#options, + options + ); + } + + duplicate< + _M extends RedisModules = M, + _F extends RedisFunctions = F, + _S extends RedisScripts = S, + _RESP extends RespVersions = RESP, + _TYPE_MAPPING extends TypeMapping = TYPE_MAPPING + >(overrides?: Partial>) { + return new (Object.getPrototypeOf(this).constructor)({ + ...this._self.#options, + commandOptions: this._commandOptions, + ...overrides + }) as RedisClientType<_M, _F, _S, _RESP, _TYPE_MAPPING>; + } + + async connect() { + await this._self.#socket.connect(); + return this as unknown as RedisClientType; + } + + sendCommand( + args: Array, + options?: CommandOptions + ): Promise { + if (!this._self.#socket.isOpen) { + return Promise.reject(new ClientClosedError()); + } else if (!this._self.#socket.isReady && this._self.#options?.disableOfflineQueue) { + return Promise.reject(new ClientOfflineError()); } - sSubscribe = this.SSUBSCRIBE; - - SUNSUBSCRIBE( - channels?: string | Array, - listener?: PubSubListener, - bufferMode?: T - ): Promise { - return this.#pubSubCommand( - this.#queue.unsubscribe( - PubSubType.SHARDED, - channels, - listener, - bufferMode - ) - ); + const promise = this._self.#queue.addCommand(args, options); + this._self.#scheduleWrite(); + return promise; + } + + async executeScript( + script: RedisScript, + args: Array, + options?: CommandOptions + ) { + try { + return await this.sendCommand(args, options); + } catch (err) { + if (!(err as Error)?.message?.startsWith?.('NOSCRIPT')) throw err; + + args[0] = 'EVAL'; + args[1] = script.SCRIPT; + return await this.sendCommand(args, options); } - - sUnsubscribe = this.SUNSUBSCRIBE; - - getPubSubListeners(type: PubSubType) { - return this.#queue.getPubSubListeners(type); + } + + async SELECT(db: number): Promise { + await this.sendCommand(['SELECT', db.toString()]); + this._self.#selectedDB = db; + } + + select = this.SELECT; + + #pubSubCommand(promise: Promise | undefined) { + if (promise === undefined) return Promise.resolve(); + + this.#scheduleWrite(); + return promise; + } + + SUBSCRIBE( + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ): Promise { + return this._self.#pubSubCommand( + this._self.#queue.subscribe( + PUBSUB_TYPE.CHANNELS, + channels, + listener, + bufferMode + ) + ); + } + + subscribe = this.SUBSCRIBE; + + UNSUBSCRIBE( + channels?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ): Promise { + return this._self.#pubSubCommand( + this._self.#queue.unsubscribe( + PUBSUB_TYPE.CHANNELS, + channels, + listener, + bufferMode + ) + ); + } + + unsubscribe = this.UNSUBSCRIBE; + + PSUBSCRIBE( + patterns: string | Array, + listener: PubSubListener, + bufferMode?: T + ): Promise { + return this._self.#pubSubCommand( + this._self.#queue.subscribe( + PUBSUB_TYPE.PATTERNS, + patterns, + listener, + bufferMode + ) + ); + } + + pSubscribe = this.PSUBSCRIBE; + + PUNSUBSCRIBE( + patterns?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ): Promise { + return this._self.#pubSubCommand( + this._self.#queue.unsubscribe( + PUBSUB_TYPE.PATTERNS, + patterns, + listener, + bufferMode + ) + ); + } + + pUnsubscribe = this.PUNSUBSCRIBE; + + SSUBSCRIBE( + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ): Promise { + return this._self.#pubSubCommand( + this._self.#queue.subscribe( + PUBSUB_TYPE.SHARDED, + channels, + listener, + bufferMode + ) + ); + } + + sSubscribe = this.SSUBSCRIBE; + + SUNSUBSCRIBE( + channels?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ): Promise { + return this._self.#pubSubCommand( + this._self.#queue.unsubscribe( + PUBSUB_TYPE.SHARDED, + channels, + listener, + bufferMode + ) + ); + } + + sUnsubscribe = this.SUNSUBSCRIBE; + + async WATCH(key: RedisVariadicArgument) { + const reply = await this._self.sendCommand( + pushVariadicArguments(['WATCH'], key) + ); + this._self.#watchEpoch ??= this._self.#epoch; + return reply as unknown as ReplyWithTypeMapping, TYPE_MAPPING>; + } + + watch = this.WATCH; + + async UNWATCH() { + const reply = await this._self.sendCommand(['UNWATCH']); + this._self.#watchEpoch = undefined; + return reply as unknown as ReplyWithTypeMapping, TYPE_MAPPING>; + } + + unwatch = this.UNWATCH; + + getPubSubListeners(type: PubSubType) { + return this._self.#queue.getPubSubListeners(type); + } + + extendPubSubChannelListeners( + type: PubSubType, + channel: string, + listeners: ChannelListeners + ) { + return this._self.#pubSubCommand( + this._self.#queue.extendPubSubChannelListeners(type, channel, listeners) + ); + } + + extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) { + return this._self.#pubSubCommand( + this._self.#queue.extendPubSubListeners(type, listeners) + ); + } + + #write() { + this.#socket.write(this.#queue.commandsToWrite()); + } + + #scheduledWrite?: NodeJS.Immediate; + + #scheduleWrite() { + if (!this.#socket.isReady || this.#scheduledWrite) return; + + this.#scheduledWrite = setImmediate(() => { + this.#write(); + this.#scheduledWrite = undefined; + }); + } + + #maybeScheduleWrite() { + if (!this.#queue.isWaitingToWrite()) return; + + this.#scheduleWrite(); + } + + /** + * @internal + */ + async _executePipeline( + commands: Array, + selectedDB?: number + ) { + if (!this._self.#socket.isOpen) { + return Promise.reject(new ClientClosedError()); } - extendPubSubChannelListeners( - type: PubSubType, - channel: string, - listeners: ChannelListeners - ) { - return this.#pubSubCommand( - this.#queue.extendPubSubChannelListeners(type, channel, listeners) - ); + const chainId = Symbol('Pipeline Chain'), + promise = Promise.all( + commands.map(({ args }) => this._self.#queue.addCommand(args, { + chainId, + typeMapping: this._commandOptions?.typeMapping + })) + ); + this._self.#scheduleWrite(); + const result = await promise; + + if (selectedDB !== undefined) { + this._self.#selectedDB = selectedDB; } - extendPubSubListeners(type: PubSubType, listeners: PubSubTypeListeners) { - return this.#pubSubCommand( - this.#queue.extendPubSubListeners(type, listeners) - ); + return result; + } + + /** + * @internal + */ + async _executeMulti( + commands: Array, + selectedDB?: number + ) { + const dirtyWatch = this._self.#dirtyWatch; + this._self.#dirtyWatch = undefined; + const watchEpoch = this._self.#watchEpoch; + this._self.#watchEpoch = undefined; + + if (!this._self.#socket.isOpen) { + throw new ClientClosedError(); } - QUIT(): Promise { - return this.#socket.quit(async () => { - if (this.#pingTimer) clearTimeout(this.#pingTimer); - const quitPromise = this.#queue.addCommand(['QUIT']); - this.#tick(); - const [reply] = await Promise.all([ - quitPromise, - this.#destroyIsolationPool() - ]); - return reply; - }); + if (dirtyWatch) { + throw new WatchError(dirtyWatch); } - quit = this.QUIT; - - #tick(force = false): void { - if (this.#socket.writableNeedDrain || (!force && !this.#socket.isReady)) { - return; - } - - this.#socket.cork(); - - while (!this.#socket.writableNeedDrain) { - const args = this.#queue.getCommandToSend(); - if (args === undefined) break; - - this.#socket.writeCommand(args); - } - } - - executeIsolated(fn: (client: RedisClientType) => T | Promise): Promise { - if (!this.#isolationPool) return Promise.reject(new ClientClosedError()); - return this.#isolationPool.use(fn); + if (watchEpoch && watchEpoch !== this._self.#epoch) { + throw new WatchError('Client reconnected after WATCH'); } - MULTI(): RedisClientMultiCommandType { - return new (this as any).Multi( - this.multiExecutor.bind(this), - this.#options?.legacyMode - ); + const typeMapping = this._commandOptions?.typeMapping; + const chainId = Symbol('MULTI Chain'); + const promises = [ + this._self.#queue.addCommand(['MULTI'], { chainId }), + ]; + + for (const { args } of commands) { + promises.push( + this._self.#queue.addCommand(args, { + chainId, + typeMapping + }) + ); } - multi = this.MULTI; - - async multiExecutor( - commands: Array, - selectedDB?: number, - chainId?: symbol - ): Promise> { - if (!this.#socket.isOpen) { - return Promise.reject(new ClientClosedError()); - } - - const promise = chainId ? - // if `chainId` has a value, it's a `MULTI` (and not "pipeline") - need to add the `MULTI` and `EXEC` commands - Promise.all([ - this.#queue.addCommand(['MULTI'], { chainId }), - this.#addMultiCommands(commands, chainId), - this.#queue.addCommand(['EXEC'], { chainId }) - ]) : - this.#addMultiCommands(commands); - - this.#tick(); + promises.push( + this._self.#queue.addCommand(['EXEC'], { chainId }) + ); - const results = await promise; + this._self.#scheduleWrite(); - if (selectedDB !== undefined) { - this.#selectedDB = selectedDB; - } + const results = await Promise.all(promises), + execResult = results[results.length - 1]; - return results; + if (execResult === null) { + throw new WatchError(); } - #addMultiCommands(commands: Array, chainId?: symbol) { - return Promise.all( - commands.map(({ args }) => this.#queue.addCommand(args, { chainId })) - ); + if (selectedDB !== undefined) { + this._self.#selectedDB = selectedDB; } - 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); + return execResult as Array; + } + + MULTI() { + type Multi = new (...args: ConstructorParameters) => RedisClientMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>; + return new ((this as any).Multi as Multi)( + this._executeMulti.bind(this), + this._executePipeline.bind(this), + this._commandOptions?.typeMapping + ); + } + + multi = this.MULTI; + + async* scanIterator( + this: RedisClientType, + options?: ScanOptions & ScanIteratorOptions + ) { + let cursor = options?.cursor ?? '0'; + do { + const reply = await this.scan(cursor, options); + cursor = reply.cursor; + yield reply.keys; + } while (cursor !== '0'); + } + + async* hScanIterator( + this: RedisClientType, + key: RedisArgument, + options?: ScanCommonOptions & ScanIteratorOptions + ) { + let cursor = options?.cursor ?? '0'; + do { + const reply = await this.hScan(key, cursor, options); + cursor = reply.cursor; + yield reply.entries; + } while (cursor !== '0'); + } + + async* hScanValuesIterator( + this: RedisClientType, + key: RedisArgument, + options?: ScanCommonOptions & ScanIteratorOptions + ) { + let cursor = options?.cursor ?? '0'; + do { + const reply = await this.hScanNoValues(key, cursor, options); + cursor = reply.cursor; + yield reply.fields; + } while (cursor !== '0'); + } + + async* hScanNoValuesIterator( + this: RedisClientType, + key: RedisArgument, + options?: ScanCommonOptions & ScanIteratorOptions + ) { + let cursor = options?.cursor ?? '0'; + do { + const reply = await this.hScanNoValues(key, cursor, options); + cursor = reply.cursor; + yield reply.fields; + } while (cursor !== '0'); + } + + async* sScanIterator( + this: RedisClientType, + key: RedisArgument, + options?: ScanCommonOptions & ScanIteratorOptions + ) { + let cursor = options?.cursor ?? '0'; + do { + const reply = await this.sScan(key, cursor, options); + cursor = reply.cursor; + yield reply.members; + } while (cursor !== '0'); + } + + async* zScanIterator( + this: RedisClientType, + key: RedisArgument, + options?: ScanCommonOptions & ScanIteratorOptions + ) { + let cursor = options?.cursor ?? '0'; + do { + const reply = await this.zScan(key, cursor, options); + cursor = reply.cursor; + yield reply.members; + } while (cursor !== '0'); + } + + async MONITOR(callback: MonitorCallback) { + const promise = this._self.#queue.monitor(callback, { + typeMapping: this._commandOptions?.typeMapping + }); + this._self.#scheduleWrite(); + await promise; + this._self.#monitorCallback = callback; + } + + monitor = this.MONITOR; + + /** + * Reset the client to its default state (i.e. stop PubSub, stop monitoring, select default DB, etc.) + */ + async reset() { + const chainId = Symbol('Reset Chain'), + promises = [this._self.#queue.reset(chainId)], + selectedDB = this._self.#options?.database ?? 0; + for (const command of this._self.#handshake(selectedDB)) { + promises.push( + this._self.#queue.addCommand(command, { + chainId + }) + ); } - - 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); + this._self.#scheduleWrite(); + await Promise.all(promises); + this._self.#selectedDB = selectedDB; + this._self.#monitorCallback = undefined; + this._self.#dirtyWatch = undefined; + this._self.#watchEpoch = undefined; + } + + /** + * If the client has state, reset it. + * An internal function to be used by wrapper class such as `RedisClientPool`. + * @internal + */ + resetIfDirty() { + let shouldReset = false; + if (this._self.#selectedDB !== (this._self.#options?.database ?? 0)) { + console.warn('Returning a client with a different selected DB'); + shouldReset = true; } - async* hScanNoValuesIterator(key: string, options?: ScanOptions): AsyncIterable> { - let cursor = 0; - do { - const reply = await (this as any).hScanNoValues(key, cursor, options); - cursor = reply.cursor; - for (const k of reply.keys) { - yield k; - } - } while (cursor !== 0); + if (this._self.#monitorCallback) { + console.warn('Returning a client with active MONITOR'); + shouldReset = true; } - 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); + if (this._self.#queue.isPubSubActive) { + console.warn('Returning a client with active PubSub'); + shouldReset = true; } - 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 { - if (this.#pingTimer) clearTimeout(this.#pingTimer); - this.#queue.flushAll(new DisconnectsClientError()); - this.#socket.disconnect(); - await this.#destroyIsolationPool(); - } - - async #destroyIsolationPool(): Promise { - await this.#isolationPool!.drain(); - await this.#isolationPool!.clear(); - this.#isolationPool = undefined; - } - - ref(): void { - this.#socket.ref(); + if (this._self.#dirtyWatch || this._self.#watchEpoch) { + console.warn('Returning a client with active WATCH'); + shouldReset = true; } - unref(): void { - this.#socket.unref(); + if (shouldReset) { + return this.reset(); } + } + + /** + * @deprecated use .close instead + */ + QUIT(): Promise { + return this._self.#socket.quit(async () => { + clearTimeout(this._self.#pingTimer); + const quitPromise = this._self.#queue.addCommand(['QUIT']); + this._self.#scheduleWrite(); + return quitPromise; + }); + } + + quit = this.QUIT; + + /** + * @deprecated use .destroy instead + */ + disconnect() { + return Promise.resolve(this.destroy()); + } + + /** + * Close the client. Wait for pending commands. + */ + close() { + return new Promise(resolve => { + clearTimeout(this._self.#pingTimer); + this._self.#socket.close(); + + if (this._self.#queue.isEmpty()) { + this._self.#socket.destroySocket(); + return resolve(); + } + + const maybeClose = () => { + if (!this._self.#queue.isEmpty()) return; + + this._self.#socket.off('data', maybeClose); + this._self.#socket.destroySocket(); + resolve(); + }; + this._self.#socket.on('data', maybeClose); + }); + } + + /** + * Destroy the client. Rejects all commands immediately. + */ + destroy() { + clearTimeout(this._self.#pingTimer); + this._self.#queue.flushAll(new DisconnectsClientError()); + this._self.#socket.destroy(); + } + + ref() { + this._self.#socket.ref(); + } + + unref() { + this._self.#socket.unref(); + } } - -attachCommands({ - BaseClass: RedisClient, - commands: COMMANDS, - executor: RedisClient.prototype.commandsExecutor -}); -(RedisClient.prototype as any).Multi = RedisClientMultiCommand; diff --git a/packages/client/lib/client/legacy-mode.spec.ts b/packages/client/lib/client/legacy-mode.spec.ts new file mode 100644 index 00000000000..306ea7f3353 --- /dev/null +++ b/packages/client/lib/client/legacy-mode.spec.ts @@ -0,0 +1,111 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { promisify } from 'node:util'; +import { RedisLegacyClientType } from './legacy-mode'; +import { ErrorReply } from '../errors'; +import { RedisClientType } from '.'; +import { once } from 'node:events'; + +function testWithLegacyClient(title: string, fn: (legacy: RedisLegacyClientType, client: RedisClientType) => Promise) { + testUtils.testWithClient(title, client => fn(client.legacy(), client), GLOBAL.SERVERS.OPEN); +} + +describe('Legacy Mode', () => { + describe('client.sendCommand', () => { + testWithLegacyClient('resolve', async client => { + assert.equal( + await promisify(client.sendCommand).call(client, 'PING'), + 'PONG' + ); + }); + + testWithLegacyClient('reject', async client => { + await assert.rejects( + promisify(client.sendCommand).call(client, 'ERROR'), + ErrorReply + ); + }); + + testWithLegacyClient('reject without a callback', async (legacy, client) => { + legacy.sendCommand('ERROR'); + const [err] = await once(client, 'error'); + assert.ok(err instanceof ErrorReply); + }); + }); + + describe('hGetAll (TRANSFORM_LEGACY_REPLY)', () => { + testWithLegacyClient('resolve', async client => { + await promisify(client.hSet).call(client, 'key', 'field', 'value'); + assert.deepEqual( + await promisify(client.hGetAll).call(client, 'key'), + Object.create(null, { + field: { + value: 'value', + configurable: true, + enumerable: true + } + }) + ); + }); + + testWithLegacyClient('reject', async client => { + await assert.rejects( + promisify(client.hGetAll).call(client), + ErrorReply + ); + }); + }); + + describe('client.set', () => { + testWithLegacyClient('vardict', async client => { + assert.equal( + await promisify(client.set).call(client, 'a', 'b'), + 'OK' + ); + }); + + testWithLegacyClient('array', async client => { + assert.equal( + await promisify(client.set).call(client, ['a', 'b']), + 'OK' + ); + }); + + testWithLegacyClient('vardict & arrays', async client => { + assert.equal( + await promisify(client.set).call(client, ['a'], 'b', ['EX', 1]), + 'OK' + ); + }); + + testWithLegacyClient('reject without a callback', async (legacy, client) => { + legacy.set('ERROR'); + const [err] = await once(client, 'error'); + assert.ok(err instanceof ErrorReply); + }); + }); + + describe('client.multi', () => { + testWithLegacyClient('resolve', async client => { + const multi = client.multi().ping().sendCommand('PING'); + assert.deepEqual( + await promisify(multi.exec).call(multi), + ['PONG', 'PONG'] + ); + }); + + testWithLegacyClient('reject', async client => { + const multi = client.multi().sendCommand('ERROR'); + await assert.rejects( + promisify(multi.exec).call(multi), + ErrorReply + ); + }); + + testWithLegacyClient('reject without a callback', async (legacy, client) => { + legacy.multi().sendCommand('ERROR').exec(); + const [err] = await once(client, 'error'); + assert.ok(err instanceof ErrorReply); + }); + }); +}); diff --git a/packages/client/lib/client/legacy-mode.ts b/packages/client/lib/client/legacy-mode.ts new file mode 100644 index 00000000000..03e7cf4efe1 --- /dev/null +++ b/packages/client/lib/client/legacy-mode.ts @@ -0,0 +1,177 @@ +import { RedisModules, RedisFunctions, RedisScripts, RespVersions, Command, CommandArguments, ReplyUnion } from '../RESP/types'; +import { RedisClientType } from '.'; +import { getTransformReply } from '../commander'; +import { ErrorReply } from '../errors'; +import COMMANDS from '../commands'; +import RedisMultiCommand from '../multi-command'; + +type LegacyArgument = string | Buffer | number | Date; + +type LegacyArguments = Array; + +type LegacyCallback = (err: ErrorReply | null, reply?: ReplyUnion) => unknown + +type LegacyCommandArguments = LegacyArguments | [ + ...args: LegacyArguments, + callback: LegacyCallback +]; + +type WithCommands = { + [P in keyof typeof COMMANDS]: (...args: LegacyCommandArguments) => void; +}; + +export type RedisLegacyClientType = RedisLegacyClient & WithCommands; + +export class RedisLegacyClient { + static #transformArguments(redisArgs: CommandArguments, args: LegacyCommandArguments) { + let callback: LegacyCallback | undefined; + if (typeof args[args.length - 1] === 'function') { + callback = args.pop() as LegacyCallback; + } + + RedisLegacyClient.pushArguments(redisArgs, args as LegacyArguments); + + return callback; + } + + static pushArguments(redisArgs: CommandArguments, args: LegacyArguments) { + for (let i = 0; i < args.length; ++i) { + const arg = args[i]; + if (Array.isArray(arg)) { + RedisLegacyClient.pushArguments(redisArgs, arg); + } else { + redisArgs.push( + typeof arg === 'number' || arg instanceof Date ? + arg.toString() : + arg + ); + } + } + } + + static getTransformReply(command: Command, resp: RespVersions) { + return command.TRANSFORM_LEGACY_REPLY ? + getTransformReply(command, resp) : + undefined; + } + + static #createCommand(name: string, command: Command, resp: RespVersions) { + const transformReply = RedisLegacyClient.getTransformReply(command, resp); + return function (this: RedisLegacyClient, ...args: LegacyCommandArguments) { + const redisArgs = [name], + callback = RedisLegacyClient.#transformArguments(redisArgs, args), + promise = this.#client.sendCommand(redisArgs); + + if (!callback) { + promise.catch(err => this.#client.emit('error', err)); + return; + } + + promise + .then(reply => callback(null, transformReply ? transformReply(reply) : reply)) + .catch(err => callback(err)); + }; + } + + #client: RedisClientType; + #Multi: ReturnType; + + constructor( + client: RedisClientType + ) { + this.#client = client; + + const RESP = client.options?.RESP ?? 2; + for (const [name, command] of Object.entries(COMMANDS)) { + // TODO: as any? + (this as any)[name] = RedisLegacyClient.#createCommand( + name, + command, + RESP + ); + } + + this.#Multi = LegacyMultiCommand.factory(RESP); + } + + sendCommand(...args: LegacyCommandArguments) { + const redisArgs: CommandArguments = [], + callback = RedisLegacyClient.#transformArguments(redisArgs, args), + promise = this.#client.sendCommand(redisArgs); + + if (!callback) { + promise.catch(err => this.#client.emit('error', err)); + return; + } + + promise + .then(reply => callback(null, reply)) + .catch(err => callback(err)); + } + + multi() { + return this.#Multi(this.#client); + } +} + +type MultiWithCommands = { + [P in keyof typeof COMMANDS]: (...args: LegacyCommandArguments) => RedisLegacyMultiType; +}; + +export type RedisLegacyMultiType = LegacyMultiCommand & MultiWithCommands; + +class LegacyMultiCommand { + static #createCommand(name: string, command: Command, resp: RespVersions) { + const transformReply = RedisLegacyClient.getTransformReply(command, resp); + return function (this: LegacyMultiCommand, ...args: LegacyArguments) { + const redisArgs = [name]; + RedisLegacyClient.pushArguments(redisArgs, args); + this.#multi.addCommand(redisArgs, transformReply); + return this; + }; + } + + static factory(resp: RespVersions) { + const Multi = class extends LegacyMultiCommand {}; + + for (const [name, command] of Object.entries(COMMANDS)) { + // TODO: as any? + (Multi as any).prototype[name] = LegacyMultiCommand.#createCommand( + name, + command, + resp + ); + } + + return (client: RedisClientType) => { + return new Multi(client) as unknown as RedisLegacyMultiType; + }; + } + + readonly #multi = new RedisMultiCommand(); + readonly #client: RedisClientType; + + constructor(client: RedisClientType) { + this.#client = client; + } + + sendCommand(...args: LegacyArguments) { + const redisArgs: CommandArguments = []; + RedisLegacyClient.pushArguments(redisArgs, args); + this.#multi.addCommand(redisArgs); + return this; + } + + exec(cb?: (err: ErrorReply | null, replies?: Array) => unknown) { + const promise = this.#client._executeMulti(this.#multi.queue); + + if (!cb) { + promise.catch(err => this.#client.emit('error', err)); + return; + } + + promise + .then(results => cb(null, this.#multi.transformReplies(results))) + .catch(err => cb?.(err)); + } +} diff --git a/packages/client/lib/client/linked-list.spec.ts b/packages/client/lib/client/linked-list.spec.ts new file mode 100644 index 00000000000..9547fb81c7c --- /dev/null +++ b/packages/client/lib/client/linked-list.spec.ts @@ -0,0 +1,138 @@ +import { SinglyLinkedList, DoublyLinkedList } from './linked-list'; +import { equal, deepEqual } from 'assert/strict'; + +describe('DoublyLinkedList', () => { + const list = new DoublyLinkedList(); + + it('should start empty', () => { + equal(list.length, 0); + equal(list.head, undefined); + equal(list.tail, undefined); + deepEqual(Array.from(list), []); + }); + + it('shift empty', () => { + equal(list.shift(), undefined); + equal(list.length, 0); + deepEqual(Array.from(list), []); + }); + + it('push 1', () => { + list.push(1); + equal(list.length, 1); + deepEqual(Array.from(list), [1]); + }); + + it('push 2', () => { + list.push(2); + equal(list.length, 2); + deepEqual(Array.from(list), [1, 2]); + }); + + it('unshift 0', () => { + list.unshift(0); + equal(list.length, 3); + deepEqual(Array.from(list), [0, 1, 2]); + }); + + it('remove middle node', () => { + list.remove(list.head!.next!); + equal(list.length, 2); + deepEqual(Array.from(list), [0, 2]); + }); + + it('remove head', () => { + list.remove(list.head!); + equal(list.length, 1); + deepEqual(Array.from(list), [2]); + }); + + it('remove tail', () => { + list.remove(list.tail!); + equal(list.length, 0); + deepEqual(Array.from(list), []); + }); + + it('unshift empty queue', () => { + list.unshift(0); + equal(list.length, 1); + deepEqual(Array.from(list), [0]); + }); + + it('push 1', () => { + list.push(1); + equal(list.length, 2); + deepEqual(Array.from(list), [0, 1]); + }); + + it('shift', () => { + equal(list.shift(), 0); + equal(list.length, 1); + deepEqual(Array.from(list), [1]); + }); + + it('shift last element', () => { + equal(list.shift(), 1); + equal(list.length, 0); + deepEqual(Array.from(list), []); + }); +}); + +describe('SinglyLinkedList', () => { + const list = new SinglyLinkedList(); + + it('should start empty', () => { + equal(list.length, 0); + equal(list.head, undefined); + equal(list.tail, undefined); + deepEqual(Array.from(list), []); + }); + + it('shift empty', () => { + equal(list.shift(), undefined); + equal(list.length, 0); + deepEqual(Array.from(list), []); + }); + + it('push 1', () => { + list.push(1); + equal(list.length, 1); + deepEqual(Array.from(list), [1]); + }); + + it('push 2', () => { + list.push(2); + equal(list.length, 2); + deepEqual(Array.from(list), [1, 2]); + }); + + it('push 3', () => { + list.push(3); + equal(list.length, 3); + deepEqual(Array.from(list), [1, 2, 3]); + }); + + it('shift 1', () => { + equal(list.shift(), 1); + equal(list.length, 2); + deepEqual(Array.from(list), [2, 3]); + }); + + it('shift 2', () => { + equal(list.shift(), 2); + equal(list.length, 1); + deepEqual(Array.from(list), [3]); + }); + + it('shift 3', () => { + equal(list.shift(), 3); + equal(list.length, 0); + deepEqual(Array.from(list), []); + }); + + it('should be empty', () => { + equal(list.length, 0); + equal(list.head, undefined); + equal(list.tail, undefined); + }); +}); diff --git a/packages/client/lib/client/linked-list.ts b/packages/client/lib/client/linked-list.ts new file mode 100644 index 00000000000..ac1d021be91 --- /dev/null +++ b/packages/client/lib/client/linked-list.ts @@ -0,0 +1,195 @@ +export interface DoublyLinkedNode { + value: T; + previous: DoublyLinkedNode | undefined; + next: DoublyLinkedNode | undefined; +} + +export class DoublyLinkedList { + #length = 0; + + get length() { + return this.#length; + } + + #head?: DoublyLinkedNode; + + get head() { + return this.#head; + } + + #tail?: DoublyLinkedNode; + + get tail() { + return this.#tail; + } + + push(value: T) { + ++this.#length; + + if (this.#tail === undefined) { + return this.#tail = this.#head = { + previous: this.#head, + next: undefined, + value + }; + } + + return this.#tail = this.#tail.next = { + previous: this.#tail, + next: undefined, + value + }; + } + + unshift(value: T) { + ++this.#length; + + if (this.#head === undefined) { + return this.#head = this.#tail = { + previous: undefined, + next: undefined, + value + }; + } + + return this.#head = this.#head.previous = { + previous: undefined, + next: this.#head, + value + }; + } + + add(value: T, prepend = false) { + return prepend ? + this.unshift(value) : + this.push(value); + } + + shift() { + if (this.#head === undefined) return undefined; + + --this.#length; + const node = this.#head; + if (node.next) { + node.next.previous = node.previous; + this.#head = node.next; + node.next = undefined; + } else { + this.#head = this.#tail = undefined; + } + return node.value; + } + + remove(node: DoublyLinkedNode) { + --this.#length; + + if (this.#tail === node) { + this.#tail = node.previous; + } + + if (this.#head === node) { + this.#head = node.next; + } else { + node.previous!.next = node.next; + node.previous = undefined; + } + + node.next = undefined; + } + + reset() { + this.#length = 0; + this.#head = this.#tail = undefined; + } + + *[Symbol.iterator]() { + let node = this.#head; + while (node !== undefined) { + yield node.value; + node = node.next; + } + } +} + +export interface SinglyLinkedNode { + value: T; + next: SinglyLinkedNode | undefined; +} + +export class SinglyLinkedList { + #length = 0; + + get length() { + return this.#length; + } + + #head?: SinglyLinkedNode; + + get head() { + return this.#head; + } + + #tail?: SinglyLinkedNode; + + get tail() { + return this.#tail; + } + + push(value: T) { + ++this.#length; + + const node = { + value, + next: undefined + }; + + if (this.#head === undefined) { + return this.#head = this.#tail = node; + } + + return this.#tail!.next = this.#tail = node; + } + + remove(node: SinglyLinkedNode, parent: SinglyLinkedNode | undefined) { + --this.#length; + + if (this.#head === node) { + if (this.#tail === node) { + this.#head = this.#tail = undefined; + } else { + this.#head = node.next; + } + } else if (this.#tail === node) { + this.#tail = parent; + parent!.next = undefined; + } else { + parent!.next = node.next; + } + } + + shift() { + if (this.#head === undefined) return undefined; + + const node = this.#head; + if (--this.#length === 0) { + this.#head = this.#tail = undefined; + } else { + this.#head = node.next; + } + + return node.value; + } + + reset() { + this.#length = 0; + this.#head = this.#tail = undefined; + } + + *[Symbol.iterator]() { + let node = this.#head; + while (node !== undefined) { + yield node.value; + node = node.next; + } + } +} diff --git a/packages/client/lib/client/multi-command.ts b/packages/client/lib/client/multi-command.ts index e347667bf2c..b6579fcf9bf 100644 --- a/packages/client/lib/client/multi-command.ts +++ b/packages/client/lib/client/multi-command.ts @@ -1,200 +1,205 @@ -import COMMANDS from './commands'; -import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction, RedisCommands } from '../commands'; -import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command'; -import { attachCommands, attachExtensions, transformLegacyCommandArguments } from '../commander'; +import COMMANDS from '../commands'; +import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType, RedisMultiQueuedCommand } from '../multi-command'; +import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping } from '../RESP/types'; +import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander'; type CommandSignature< - C extends RedisCommand, - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = (...args: Parameters) => RedisClientMultiCommandType; + REPLIES extends Array, + C extends Command, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = (...args: Parameters) => RedisClientMultiCommandType< + [...REPLIES, ReplyWithTypeMapping, TYPE_MAPPING>], + M, + F, + S, + RESP, + TYPE_MAPPING +>; type WithCommands< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > = { - [P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], M, F, S>; + [P in keyof typeof COMMANDS]: CommandSignature; }; type WithModules< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > = { - [P in keyof M as ExcludeMappedString

]: { - [C in keyof M[P] as ExcludeMappedString]: CommandSignature; - }; + [P in keyof M]: { + [C in keyof M[P]]: CommandSignature; + }; }; type WithFunctions< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > = { - [P in keyof F as ExcludeMappedString

]: { - [FF in keyof F[P] as ExcludeMappedString]: CommandSignature; - }; + [L in keyof F]: { + [C in keyof F[L]]: CommandSignature; + }; }; type WithScripts< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > = { - [P in keyof S as ExcludeMappedString

]: CommandSignature; + [P in keyof S]: CommandSignature; }; export type RedisClientMultiCommandType< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = RedisClientMultiCommand & WithCommands & WithModules & WithFunctions & WithScripts; - -type InstantiableRedisMultiCommand< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = new (...args: ConstructorParameters) => RedisClientMultiCommandType; - -export type RedisClientMultiExecutor = ( - queue: Array, - selectedDB?: number, - chainId?: symbol -) => Promise>; - -export default class RedisClientMultiCommand { - static extend< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >(extensions?: RedisExtensions): InstantiableRedisMultiCommand { - return attachExtensions({ - BaseClass: RedisClientMultiCommand, - modulesExecutor: RedisClientMultiCommand.prototype.commandsExecutor, - modules: extensions?.modules, - functionsExecutor: RedisClientMultiCommand.prototype.functionsExecutor, - functions: extensions?.functions, - scriptsExecutor: RedisClientMultiCommand.prototype.scriptsExecutor, - scripts: extensions?.scripts - }); - } - - readonly #multi = new RedisMultiCommand(); - readonly #executor: RedisClientMultiExecutor; - readonly v4: Record = {}; - #selectedDB?: number; - - constructor(executor: RedisClientMultiExecutor, legacyMode = false) { - this.#executor = executor; - if (legacyMode) { - this.#legacyMode(); - } - } - - #legacyMode(): void { - this.v4.addCommand = this.addCommand.bind(this); - (this as any).addCommand = (...args: Array): this => { - this.#multi.addCommand(transformLegacyCommandArguments(args)); - return this; - }; - this.v4.exec = this.exec.bind(this); - (this as any).exec = (callback?: (err: Error | null, replies?: Array) => unknown): 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, command ] of Object.entries(COMMANDS as RedisCommands)) { - this.#defineLegacyCommand(name, command); - (this as any)[name.toLowerCase()] ??= (this as any)[name]; - } - } - - #defineLegacyCommand(this: any, name: string, command?: RedisCommand): void { - this.v4[name] = this[name].bind(this.v4); - this[name] = command && command.TRANSFORM_LEGACY_REPLY && command.transformReply ? - (...args: Array) => { - this.#multi.addCommand( - [name, ...transformLegacyCommandArguments(args)], - command.transformReply - ); - return this; - } : - (...args: Array) => this.addCommand(name, ...args); - } - - commandsExecutor(command: RedisCommand, args: Array): this { - return this.addCommand( - command.transformArguments(...args), - command.transformReply - ); - } - - SELECT(db: number, transformReply?: RedisCommand['transformReply']): this { - this.#selectedDB = db; - return this.addCommand(['SELECT', db.toString()], transformReply); - } - - select = this.SELECT; - - addCommand(args: RedisCommandArguments, transformReply?: RedisCommand['transformReply']): this { - this.#multi.addCommand(args, transformReply); - return this; - } - - functionsExecutor(fn: RedisFunction, args: Array, name: string): this { - this.#multi.addFunction(name, fn, args); - return this; - } - - scriptsExecutor(script: RedisScript, args: Array): this { - this.#multi.addScript(script, args); - return this; - } - - async exec(execAsPipeline = false): Promise> { - if (execAsPipeline) { - return this.execAsPipeline(); - } - - return this.#multi.handleExecReplies( - await this.#executor( - this.#multi.queue, - this.#selectedDB, - RedisMultiCommand.generateChainId() - ) - ); - } - - EXEC = this.exec; - - async execAsPipeline(): Promise> { - if (this.#multi.queue.length === 0) return []; - - return this.#multi.transformReplies( - await this.#executor( - this.#multi.queue, - this.#selectedDB - ) - ); - } + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = ( + RedisClientMultiCommand & + WithCommands & + WithModules & + WithFunctions & + WithScripts +); + +type ExecuteMulti = (commands: Array, selectedDB?: number) => Promise>; + +export default class RedisClientMultiCommand { + static #createCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return function (this: RedisClientMultiCommand, ...args: Array) { + return this.addCommand( + command.transformArguments(...args), + transformReply + ); + }; + } + + static #createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return function (this: { _self: RedisClientMultiCommand }, ...args: Array) { + return this._self.addCommand( + command.transformArguments(...args), + transformReply + ); + }; + } + + static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn), + transformReply = getTransformReply(fn, resp); + return function (this: { _self: RedisClientMultiCommand }, ...args: Array) { + const fnArgs = fn.transformArguments(...args), + redisArgs: CommandArguments = prefix.concat(fnArgs); + redisArgs.preserve = fnArgs.preserve; + return this._self.addCommand( + redisArgs, + transformReply + ); + }; + } + + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const transformReply = getTransformReply(script, resp); + return function (this: RedisClientMultiCommand, ...args: Array) { + this.#multi.addScript( + script, + script.transformArguments(...args), + transformReply + ); + return this; + }; + } + + static extend< + M extends RedisModules = Record, + F extends RedisFunctions = Record, + S extends RedisScripts = Record, + RESP extends RespVersions = 2 + >(config?: CommanderConfig) { + return attachConfig({ + BaseClass: RedisClientMultiCommand, + commands: COMMANDS, + createCommand: RedisClientMultiCommand.#createCommand, + createModuleCommand: RedisClientMultiCommand.#createModuleCommand, + createFunctionCommand: RedisClientMultiCommand.#createFunctionCommand, + createScriptCommand: RedisClientMultiCommand.#createScriptCommand, + config + }); + } + + readonly #multi = new RedisMultiCommand(); + readonly #executeMulti: ExecuteMulti; + readonly #executePipeline: ExecuteMulti; + readonly #typeMapping?: TypeMapping; + + #selectedDB?: number; + + constructor(executeMulti: ExecuteMulti, executePipeline: ExecuteMulti, typeMapping?: TypeMapping) { + this.#executeMulti = executeMulti; + this.#executePipeline = executePipeline; + this.#typeMapping = typeMapping; + } + + SELECT(db: number, transformReply?: TransformReply): this { + this.#selectedDB = db; + this.#multi.addCommand(['SELECT', db.toString()], transformReply); + return this; + } + + select = this.SELECT; + + addCommand(args: CommandArguments, transformReply?: TransformReply) { + this.#multi.addCommand(args, transformReply); + return this; + } + + async exec(execAsPipeline = false): Promise> { + if (execAsPipeline) return this.execAsPipeline(); + + return this.#multi.transformReplies( + await this.#executeMulti(this.#multi.queue, this.#selectedDB), + this.#typeMapping + ) as MultiReplyType; + } + + EXEC = this.exec; + + execTyped(execAsPipeline = false) { + return this.exec(execAsPipeline); + } + + async execAsPipeline(): Promise> { + if (this.#multi.queue.length === 0) return [] as MultiReplyType; + + return this.#multi.transformReplies( + await this.#executePipeline(this.#multi.queue, this.#selectedDB), + this.#typeMapping + ) as MultiReplyType; + } + + execAsPipelineTyped() { + return this.execAsPipeline(); + } } - -attachCommands({ - BaseClass: RedisClientMultiCommand, - commands: COMMANDS, - executor: RedisClientMultiCommand.prototype.commandsExecutor -}); diff --git a/packages/client/lib/client/pool.spec.ts b/packages/client/lib/client/pool.spec.ts new file mode 100644 index 00000000000..8fc7a258df9 --- /dev/null +++ b/packages/client/lib/client/pool.spec.ts @@ -0,0 +1,11 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; + +describe('RedisClientPool', () => { + testUtils.testWithClientPool('sendCommand', async pool => { + assert.equal( + await pool.sendCommand(['PING']), + 'PONG' + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/client/pool.ts b/packages/client/lib/client/pool.ts new file mode 100644 index 00000000000..4bd99ece8b6 --- /dev/null +++ b/packages/client/lib/client/pool.ts @@ -0,0 +1,489 @@ +import COMMANDS from '../commands'; +import { Command, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; +import RedisClient, { RedisClientType, RedisClientOptions, RedisClientExtensions } from '.'; +import { EventEmitter } from 'node:events'; +import { DoublyLinkedNode, DoublyLinkedList, SinglyLinkedList } from './linked-list'; +import { TimeoutError } from '../errors'; +import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; +import { CommandOptions } from './commands-queue'; +import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; + +export interface RedisPoolOptions { + /** + * The minimum number of clients to keep in the pool (>= 1). + */ + minimum: number; + /** + * The maximum number of clients to keep in the pool (>= {@link RedisPoolOptions.minimum} >= 1). + */ + maximum: number; + /** + * The maximum time a task can wait for a client to become available (>= 0). + */ + acquireTimeout: number; + /** + * TODO + */ + cleanupDelay: number; + /** + * TODO + */ + unstableResp3Modules?: boolean; +} + +export type PoolTask< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping, + T = unknown +> = (client: RedisClientType) => T; + +export type RedisClientPoolType< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} +> = ( + RedisClientPool & + RedisClientExtensions +); + +type ProxyPool = RedisClientPoolType; + +type NamespaceProxyPool = { _self: ProxyPool }; + +export class RedisClientPool< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} +> extends EventEmitter { + static #createCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return async function (this: ProxyPool, ...args: Array) { + const redisArgs = command.transformArguments(...args); + const typeMapping = this._commandOptions?.typeMapping; + + const reply = await this.sendCommand(redisArgs, this._commandOptions); + + return transformReply ? + transformReply(reply, redisArgs.preserve, typeMapping) : + reply; + }; + } + + static #createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return async function (this: NamespaceProxyPool, ...args: Array) { + const redisArgs = command.transformArguments(...args); + const typeMapping = this._self._commandOptions?.typeMapping; + + const reply = await this._self.sendCommand(redisArgs, this._self._commandOptions); + + return transformReply ? + transformReply(reply, redisArgs.preserve, typeMapping) : + reply; + }; + } + + static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn), + transformReply = getTransformReply(fn, resp); + return async function (this: NamespaceProxyPool, ...args: Array) { + const fnArgs = fn.transformArguments(...args); + const typeMapping = this._self._commandOptions?.typeMapping; + + const reply = await this._self.sendCommand( + prefix.concat(fnArgs), + this._self._commandOptions + ); + + return transformReply ? + transformReply(reply, fnArgs.preserve, typeMapping) : + reply; + }; + } + + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const prefix = scriptArgumentsPrefix(script), + transformReply = getTransformReply(script, resp); + return async function (this: ProxyPool, ...args: Array) { + const scriptArgs = script.transformArguments(...args); + const redisArgs = prefix.concat(scriptArgs); + const typeMapping = this._commandOptions?.typeMapping; + + const reply = await this.executeScript(script, redisArgs, this._commandOptions); + + return transformReply ? + transformReply(reply, scriptArgs.preserve, typeMapping) : + reply; + }; + } + + static create< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping = {} + >( + clientOptions?: RedisClientOptions, + options?: Partial + ) { + const Pool = attachConfig({ + BaseClass: RedisClientPool, + commands: COMMANDS, + createCommand: RedisClientPool.#createCommand, + createModuleCommand: RedisClientPool.#createModuleCommand, + createFunctionCommand: RedisClientPool.#createFunctionCommand, + createScriptCommand: RedisClientPool.#createScriptCommand, + config: clientOptions + }); + + Pool.prototype.Multi = RedisClientMultiCommand.extend(clientOptions); + + // returning a "proxy" to prevent the namespaces._self to leak between "proxies" + return Object.create( + new Pool( + RedisClient.factory(clientOptions).bind(undefined, clientOptions), + options + ) + ) as RedisClientPoolType; + } + + // TODO: defaults + static #DEFAULTS = { + minimum: 1, + maximum: 100, + acquireTimeout: 3000, + cleanupDelay: 3000 + } satisfies RedisPoolOptions; + + readonly #clientFactory: () => RedisClientType; + readonly #options: RedisPoolOptions; + + readonly #idleClients = new SinglyLinkedList>(); + + /** + * The number of idle clients. + */ + get idleClients() { + return this._self.#idleClients.length; + } + + readonly #clientsInUse = new DoublyLinkedList>(); + + /** + * The number of clients in use. + */ + get clientsInUse() { + return this._self.#clientsInUse.length; + } + + /** + * The total number of clients in the pool (including connecting, idle, and in use). + */ + get totalClients() { + return this._self.#idleClients.length + this._self.#clientsInUse.length; + } + + readonly #tasksQueue = new SinglyLinkedList<{ + timeout: NodeJS.Timeout | undefined; + resolve: (value: unknown) => unknown; + reject: (reason?: unknown) => unknown; + fn: PoolTask; + }>(); + + /** + * The number of tasks waiting for a client to become available. + */ + get tasksQueueLength() { + return this._self.#tasksQueue.length; + } + + #isOpen = false; + + /** + * Whether the pool is open (either connecting or connected). + */ + get isOpen() { + return this._self.#isOpen; + } + + #isClosing = false; + + /** + * Whether the pool is closing (*not* closed). + */ + get isClosing() { + return this._self.#isClosing; + } + + /** + * You are probably looking for {@link RedisClient.createPool `RedisClient.createPool`}, + * {@link RedisClientPool.fromClient `RedisClientPool.fromClient`}, + * or {@link RedisClientPool.fromOptions `RedisClientPool.fromOptions`}... + */ + constructor( + clientFactory: () => RedisClientType, + options?: Partial + ) { + super(); + + this.#clientFactory = clientFactory; + this.#options = { + ...RedisClientPool.#DEFAULTS, + ...options + }; + } + + private _self = this; + private _commandOptions?: CommandOptions; + + withCommandOptions< + OPTIONS extends CommandOptions, + TYPE_MAPPING extends TypeMapping + >(options: OPTIONS) { + const proxy = Object.create(this._self); + proxy._commandOptions = options; + return proxy as RedisClientPoolType< + M, + F, + S, + RESP, + TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {} + >; + } + + #commandOptionsProxy< + K extends keyof CommandOptions, + V extends CommandOptions[K] + >( + key: K, + value: V + ) { + const proxy = Object.create(this._self); + proxy._commandOptions = Object.create(this._commandOptions ?? null); + proxy._commandOptions[key] = value; + return proxy as RedisClientPoolType< + M, + F, + S, + RESP, + K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING + >; + } + + /** + * Override the `typeMapping` command option + */ + withTypeMapping(typeMapping: TYPE_MAPPING) { + return this._self.#commandOptionsProxy('typeMapping', typeMapping); + } + + /** + * Override the `abortSignal` command option + */ + withAbortSignal(abortSignal: AbortSignal) { + return this._self.#commandOptionsProxy('abortSignal', abortSignal); + } + + /** + * Override the `asap` command option to `true` + * TODO: remove? + */ + asap() { + return this._self.#commandOptionsProxy('asap', true); + } + + async connect() { + if (this._self.#isOpen) return; // TODO: throw error? + + this._self.#isOpen = true; + + const promises = []; + while (promises.length < this._self.#options.minimum) { + promises.push(this._self.#create()); + } + + try { + await Promise.all(promises); + return this as unknown as RedisClientPoolType; + } catch (err) { + this.destroy(); + throw err; + } + } + + async #create() { + const node = this._self.#clientsInUse.push( + this._self.#clientFactory() + .on('error', (err: Error) => this.emit('error', err)) + ); + + try { + await node.value.connect(); + } catch (err) { + this._self.#clientsInUse.remove(node); + throw err; + } + + this._self.#returnClient(node); + } + + execute(fn: PoolTask) { + return new Promise>((resolve, reject) => { + const client = this._self.#idleClients.shift(), + { tail } = this._self.#tasksQueue; + if (!client) { + let timeout; + if (this._self.#options.acquireTimeout > 0) { + timeout = setTimeout( + () => { + this._self.#tasksQueue.remove(task, tail); + reject(new TimeoutError('Timeout waiting for a client')); // TODO: message + }, + this._self.#options.acquireTimeout + ); + } + + const task = this._self.#tasksQueue.push({ + timeout, + // @ts-ignore + resolve, + reject, + fn + }); + + if (this.totalClients < this._self.#options.maximum) { + this._self.#create(); + } + + return; + } + + const node = this._self.#clientsInUse.push(client); + // @ts-ignore + this._self.#executeTask(node, resolve, reject, fn); + }); + } + + #executeTask( + node: DoublyLinkedNode>, + resolve: (value: T | PromiseLike) => void, + reject: (reason?: unknown) => void, + fn: PoolTask + ) { + const result = fn(node.value); + if (result instanceof Promise) { + result.then(resolve, reject); + result.finally(() => this.#returnClient(node)) + } else { + resolve(result); + this.#returnClient(node); + } + } + + #returnClient(node: DoublyLinkedNode>) { + const task = this.#tasksQueue.shift(); + if (task) { + clearTimeout(task.timeout); + this.#executeTask(node, task.resolve, task.reject, task.fn); + return; + } + + this.#clientsInUse.remove(node); + this.#idleClients.push(node.value); + + this.#scheduleCleanup(); + } + + cleanupTimeout?: NodeJS.Timeout; + + #scheduleCleanup() { + if (this.totalClients <= this.#options.minimum) return; + + clearTimeout(this.cleanupTimeout); + this.cleanupTimeout = setTimeout(() => this.#cleanup(), this.#options.cleanupDelay); + } + + #cleanup() { + const toDestroy = Math.min(this.#idleClients.length, this.totalClients - this.#options.minimum); + for (let i = 0; i < toDestroy; i++) { + // TODO: shift vs pop + this.#idleClients.shift()!.destroy(); + } + } + + sendCommand( + args: Array, + options?: CommandOptions + ) { + return this.execute(client => client.sendCommand(args, options)); + } + + executeScript( + script: RedisScript, + args: Array, + options?: CommandOptions + ) { + return this.execute(client => client.executeScript(script, args, options)); + } + + MULTI() { + type Multi = new (...args: ConstructorParameters) => RedisClientMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>; + return new ((this as any).Multi as Multi)( + (commands, selectedDB) => this.execute(client => client._executeMulti(commands, selectedDB)), + commands => this.execute(client => client._executePipeline(commands)), + this._commandOptions?.typeMapping + ); + } + + multi = this.MULTI; + + async close() { + if (this._self.#isClosing) return; // TODO: throw err? + if (!this._self.#isOpen) return; // TODO: throw err? + + this._self.#isClosing = true; + + try { + const promises = []; + + for (const client of this._self.#idleClients) { + promises.push(client.close()); + } + + for (const client of this._self.#clientsInUse) { + promises.push(client.close()); + } + + await Promise.all(promises); + + this._self.#idleClients.reset(); + this._self.#clientsInUse.reset(); + } catch (err) { + + } finally { + this._self.#isClosing = false; + } + } + + destroy() { + for (const client of this._self.#idleClients) { + client.destroy(); + } + this._self.#idleClients.reset(); + + for (const client of this._self.#clientsInUse) { + client.destroy(); + } + this._self.#clientsInUse.reset(); + + this._self.#isOpen = false; + } +} diff --git a/packages/client/lib/client/pub-sub.spec.ts b/packages/client/lib/client/pub-sub.spec.ts index 8b9f16732cb..74bd85c1831 100644 --- a/packages/client/lib/client/pub-sub.spec.ts +++ b/packages/client/lib/client/pub-sub.spec.ts @@ -1,151 +1,151 @@ -import { strict as assert } from 'assert'; -import { PubSub, PubSubType } from './pub-sub'; +import { strict as assert } from 'node:assert'; +import { PubSub, PUBSUB_TYPE } from './pub-sub'; describe('PubSub', () => { - const TYPE = PubSubType.CHANNELS, - CHANNEL = 'channel', - LISTENER = () => {}; - - describe('subscribe to new channel', () => { - function createAndSubscribe() { - const pubSub = new PubSub(), - command = pubSub.subscribe(TYPE, CHANNEL, LISTENER); - - assert.equal(pubSub.isActive, true); - assert.ok(command); - assert.equal(command.channelsCounter, 1); - - return { - pubSub, - command - }; - } - - it('resolve', () => { - const { pubSub, command } = createAndSubscribe(); - - command.resolve(); - - assert.equal(pubSub.isActive, true); - }); - - it('reject', () => { - const { pubSub, command } = createAndSubscribe(); - - assert.ok(command.reject); - command.reject(); - - assert.equal(pubSub.isActive, false); - }); + const TYPE = PUBSUB_TYPE.CHANNELS, + CHANNEL = 'channel', + LISTENER = () => {}; + + describe('subscribe to new channel', () => { + function createAndSubscribe() { + const pubSub = new PubSub(), + command = pubSub.subscribe(TYPE, CHANNEL, LISTENER); + + assert.equal(pubSub.isActive, true); + assert.ok(command); + assert.equal(command.channelsCounter, 1); + + return { + pubSub, + command + }; + } + + it('resolve', () => { + const { pubSub, command } = createAndSubscribe(); + + command.resolve(); + + assert.equal(pubSub.isActive, true); }); - it('subscribe to already subscribed channel', () => { - const pubSub = new PubSub(), - firstSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); - assert.ok(firstSubscribe); + it('reject', () => { + const { pubSub, command } = createAndSubscribe(); - const secondSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); - assert.ok(secondSubscribe); + assert.ok(command.reject); + command.reject(); - firstSubscribe.resolve(); + assert.equal(pubSub.isActive, false); + }); + }); + + it('subscribe to already subscribed channel', () => { + const pubSub = new PubSub(), + firstSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); + assert.ok(firstSubscribe); + + const secondSubscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); + assert.ok(secondSubscribe); + + firstSubscribe.resolve(); + + assert.equal( + pubSub.subscribe(TYPE, CHANNEL, LISTENER), + undefined + ); + }); + + it('unsubscribe all', () => { + const pubSub = new PubSub(); + + const subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); + assert.ok(subscribe); + subscribe.resolve(); + assert.equal(pubSub.isActive, true); + + const unsubscribe = pubSub.unsubscribe(TYPE); + assert.equal(pubSub.isActive, true); + assert.ok(unsubscribe); + unsubscribe.resolve(); + assert.equal(pubSub.isActive, false); + }); + + describe('unsubscribe from channel', () => { + it('when not subscribed', () => { + const pubSub = new PubSub(), + unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL); + assert.ok(unsubscribe); + unsubscribe.resolve(); + assert.equal(pubSub.isActive, false); + }); - assert.equal( - pubSub.subscribe(TYPE, CHANNEL, LISTENER), - undefined - ); + it('when already subscribed', () => { + const pubSub = new PubSub(), + subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); + assert.ok(subscribe); + subscribe.resolve(); + assert.equal(pubSub.isActive, true); + + const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL); + assert.equal(pubSub.isActive, true); + assert.ok(unsubscribe); + unsubscribe.resolve(); + assert.equal(pubSub.isActive, false); + }); + }); + + describe('unsubscribe from listener', () => { + it('when it\'s the only listener', () => { + const pubSub = new PubSub(), + subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); + assert.ok(subscribe); + subscribe.resolve(); + assert.equal(pubSub.isActive, true); + + const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL, LISTENER); + assert.ok(unsubscribe); + unsubscribe.resolve(); + assert.equal(pubSub.isActive, false); }); - it('unsubscribe all', () => { - const pubSub = new PubSub(); - - const subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); + it('when there are more listeners', () => { + const pubSub = new PubSub(), + subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); + assert.ok(subscribe); + subscribe.resolve(); + assert.equal(pubSub.isActive, true); + + assert.equal( + pubSub.subscribe(TYPE, CHANNEL, () => { }), + undefined + ); + + assert.equal( + pubSub.unsubscribe(TYPE, CHANNEL, LISTENER), + undefined + ); + }); + + describe('non-existing listener', () => { + it('on subscribed channel', () => { + const pubSub = new PubSub(), + subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); assert.ok(subscribe); subscribe.resolve(); assert.equal(pubSub.isActive, true); - const unsubscribe = pubSub.unsubscribe(TYPE); + assert.equal( + pubSub.unsubscribe(TYPE, CHANNEL, () => { }), + undefined + ); assert.equal(pubSub.isActive, true); - assert.ok(unsubscribe); - unsubscribe.resolve(); - assert.equal(pubSub.isActive, false); - }); - - describe('unsubscribe from channel', () => { - it('when not subscribed', () => { - const pubSub = new PubSub(), - unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL); - assert.ok(unsubscribe); - unsubscribe.resolve(); - assert.equal(pubSub.isActive, false); - }); - - it('when already subscribed', () => { - const pubSub = new PubSub(), - subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); - assert.ok(subscribe); - subscribe.resolve(); - assert.equal(pubSub.isActive, true); - - const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL); - assert.equal(pubSub.isActive, true); - assert.ok(unsubscribe); - unsubscribe.resolve(); - assert.equal(pubSub.isActive, false); - }); - }); + }); - describe('unsubscribe from listener', () => { - it('when it\'s the only listener', () => { - const pubSub = new PubSub(), - subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); - assert.ok(subscribe); - subscribe.resolve(); - assert.equal(pubSub.isActive, true); - - const unsubscribe = pubSub.unsubscribe(TYPE, CHANNEL, LISTENER); - assert.ok(unsubscribe); - unsubscribe.resolve(); - assert.equal(pubSub.isActive, false); - }); - - it('when there are more listeners', () => { - const pubSub = new PubSub(), - subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); - assert.ok(subscribe); - subscribe.resolve(); - assert.equal(pubSub.isActive, true); - - assert.equal( - pubSub.subscribe(TYPE, CHANNEL, () => {}), - undefined - ); - - assert.equal( - pubSub.unsubscribe(TYPE, CHANNEL, LISTENER), - undefined - ); - }); - - describe('non-existing listener', () => { - it('on subscribed channel', () => { - const pubSub = new PubSub(), - subscribe = pubSub.subscribe(TYPE, CHANNEL, LISTENER); - assert.ok(subscribe); - subscribe.resolve(); - assert.equal(pubSub.isActive, true); - - assert.equal( - pubSub.unsubscribe(TYPE, CHANNEL, () => {}), - undefined - ); - assert.equal(pubSub.isActive, true); - }); - - it('on unsubscribed channel', () => { - const pubSub = new PubSub(); - assert.ok(pubSub.unsubscribe(TYPE, CHANNEL, () => {})); - assert.equal(pubSub.isActive, false); - }); - }); + it('on unsubscribed channel', () => { + const pubSub = new PubSub(); + assert.ok(pubSub.unsubscribe(TYPE, CHANNEL, () => { })); + assert.equal(pubSub.isActive, false); + }); }); + }); }); diff --git a/packages/client/lib/client/pub-sub.ts b/packages/client/lib/client/pub-sub.ts index a8a909e0252..1387aea8417 100644 --- a/packages/client/lib/client/pub-sub.ts +++ b/packages/client/lib/client/pub-sub.ts @@ -1,408 +1,409 @@ -import { RedisCommandArgument } from "../commands"; +import { RedisArgument } from '../RESP/types'; +import { CommandToWrite } from './commands-queue'; -export enum PubSubType { - CHANNELS = 'CHANNELS', - PATTERNS = 'PATTERNS', - SHARDED = 'SHARDED' -} +export const PUBSUB_TYPE = { + CHANNELS: 'CHANNELS', + PATTERNS: 'PATTERNS', + SHARDED: 'SHARDED' +} as const; + +export type PUBSUB_TYPE = typeof PUBSUB_TYPE; + +export type PubSubType = PUBSUB_TYPE[keyof PUBSUB_TYPE]; const COMMANDS = { - [PubSubType.CHANNELS]: { - subscribe: Buffer.from('subscribe'), - unsubscribe: Buffer.from('unsubscribe'), - message: Buffer.from('message') - }, - [PubSubType.PATTERNS]: { - subscribe: Buffer.from('psubscribe'), - unsubscribe: Buffer.from('punsubscribe'), - message: Buffer.from('pmessage') - }, - [PubSubType.SHARDED]: { - subscribe: Buffer.from('ssubscribe'), - unsubscribe: Buffer.from('sunsubscribe'), - message: Buffer.from('smessage') - } + [PUBSUB_TYPE.CHANNELS]: { + subscribe: Buffer.from('subscribe'), + unsubscribe: Buffer.from('unsubscribe'), + message: Buffer.from('message') + }, + [PUBSUB_TYPE.PATTERNS]: { + subscribe: Buffer.from('psubscribe'), + unsubscribe: Buffer.from('punsubscribe'), + message: Buffer.from('pmessage') + }, + [PUBSUB_TYPE.SHARDED]: { + subscribe: Buffer.from('ssubscribe'), + unsubscribe: Buffer.from('sunsubscribe'), + message: Buffer.from('smessage') + } }; export type PubSubListener< - RETURN_BUFFERS extends boolean = false + RETURN_BUFFERS extends boolean = false > = (message: T, channel: T) => unknown; export interface ChannelListeners { - unsubscribing: boolean; - buffers: Set>; - strings: Set>; + unsubscribing: boolean; + buffers: Set>; + strings: Set>; } export type PubSubTypeListeners = Map; -type Listeners = Record; +export type PubSubListeners = Record; -export type PubSubCommand = ReturnType< - typeof PubSub.prototype.subscribe | - typeof PubSub.prototype.unsubscribe | - typeof PubSub.prototype.extendTypeListeners ->; +export type PubSubCommand = ( + Required> & { + reject: undefined | (() => unknown); + } +); export class PubSub { - static isStatusReply(reply: Array): boolean { - return ( - COMMANDS[PubSubType.CHANNELS].subscribe.equals(reply[0]) || - COMMANDS[PubSubType.CHANNELS].unsubscribe.equals(reply[0]) || - COMMANDS[PubSubType.PATTERNS].subscribe.equals(reply[0]) || - COMMANDS[PubSubType.PATTERNS].unsubscribe.equals(reply[0]) || - COMMANDS[PubSubType.SHARDED].subscribe.equals(reply[0]) - ); - } - - static isShardedUnsubscribe(reply: Array): boolean { - return COMMANDS[PubSubType.SHARDED].unsubscribe.equals(reply[0]); + static isStatusReply(reply: Array): boolean { + return ( + COMMANDS[PUBSUB_TYPE.CHANNELS].subscribe.equals(reply[0]) || + COMMANDS[PUBSUB_TYPE.CHANNELS].unsubscribe.equals(reply[0]) || + COMMANDS[PUBSUB_TYPE.PATTERNS].subscribe.equals(reply[0]) || + COMMANDS[PUBSUB_TYPE.PATTERNS].unsubscribe.equals(reply[0]) || + COMMANDS[PUBSUB_TYPE.SHARDED].subscribe.equals(reply[0]) + ); + } + + static isShardedUnsubscribe(reply: Array): boolean { + return COMMANDS[PUBSUB_TYPE.SHARDED].unsubscribe.equals(reply[0]); + } + + static #channelsArray(channels: string | Array) { + return (Array.isArray(channels) ? channels : [channels]); + } + + static #listenersSet( + listeners: ChannelListeners, + returnBuffers?: T + ) { + return (returnBuffers ? listeners.buffers : listeners.strings); + } + + #subscribing = 0; + + #isActive = false; + + get isActive() { + return this.#isActive; + } + + readonly listeners: PubSubListeners = { + [PUBSUB_TYPE.CHANNELS]: new Map(), + [PUBSUB_TYPE.PATTERNS]: new Map(), + [PUBSUB_TYPE.SHARDED]: new Map() + }; + + subscribe( + type: PubSubType, + channels: string | Array, + listener: PubSubListener, + returnBuffers?: T + ) { + const args: Array = [COMMANDS[type].subscribe], + channelsArray = PubSub.#channelsArray(channels); + for (const channel of channelsArray) { + let channelListeners = this.listeners[type].get(channel); + if (!channelListeners || channelListeners.unsubscribing) { + args.push(channel); + } } - - static #channelsArray(channels: string | Array) { - return (Array.isArray(channels) ? channels : [channels]); - } - - static #listenersSet( - listeners: ChannelListeners, - returnBuffers?: T - ) { - return (returnBuffers ? listeners.buffers : listeners.strings); - } - - #subscribing = 0; - - #isActive = false; - get isActive() { - return this.#isActive; + if (args.length === 1) { + // all channels are already subscribed, add listeners without issuing a command + for (const channel of channelsArray) { + PubSub.#listenersSet( + this.listeners[type].get(channel)!, + returnBuffers + ).add(listener); + } + return; } - #listeners: Listeners = { - [PubSubType.CHANNELS]: new Map(), - [PubSubType.PATTERNS]: new Map(), - [PubSubType.SHARDED]: new Map() - }; - - subscribe( - type: PubSubType, - channels: string | Array, - listener: PubSubListener, - returnBuffers?: T - ) { - const args: Array = [COMMANDS[type].subscribe], - channelsArray = PubSub.#channelsArray(channels); + this.#isActive = true; + this.#subscribing++; + return { + args, + channelsCounter: args.length - 1, + resolve: () => { + this.#subscribing--; for (const channel of channelsArray) { - let channelListeners = this.#listeners[type].get(channel); - if (!channelListeners || channelListeners.unsubscribing) { - args.push(channel); - } + let listeners = this.listeners[type].get(channel); + if (!listeners) { + listeners = { + unsubscribing: false, + buffers: new Set(), + strings: new Set() + }; + this.listeners[type].set(channel, listeners); + } + + PubSub.#listenersSet(listeners, returnBuffers).add(listener); } - - if (args.length === 1) { - // all channels are already subscribed, add listeners without issuing a command - for (const channel of channelsArray) { - PubSub.#listenersSet( - this.#listeners[type].get(channel)!, - returnBuffers - ).add(listener); - } - return; - } - - this.#isActive = true; - this.#subscribing++; - return { - args, - channelsCounter: args.length - 1, - resolve: () => { - this.#subscribing--; - for (const channel of channelsArray) { - let listeners = this.#listeners[type].get(channel); - if (!listeners) { - listeners = { - unsubscribing: false, - buffers: new Set(), - strings: new Set() - }; - this.#listeners[type].set(channel, listeners); - } - - PubSub.#listenersSet(listeners, returnBuffers).add(listener); - } - }, - reject: () => { - this.#subscribing--; - this.#updateIsActive(); - } - }; + }, + reject: () => { + this.#subscribing--; + this.#updateIsActive(); + } + } satisfies PubSubCommand; + } + + extendChannelListeners( + type: PubSubType, + channel: string, + listeners: ChannelListeners + ) { + if (!this.#extendChannelListeners(type, channel, listeners)) return; + + this.#isActive = true; + this.#subscribing++; + return { + args: [ + COMMANDS[type].subscribe, + channel + ], + channelsCounter: 1, + resolve: () => this.#subscribing--, + reject: () => { + this.#subscribing--; + this.#updateIsActive(); + } + } satisfies PubSubCommand; + } + + #extendChannelListeners( + type: PubSubType, + channel: string, + listeners: ChannelListeners + ) { + const existingListeners = this.listeners[type].get(channel); + if (!existingListeners) { + this.listeners[type].set(channel, listeners); + return true; } - extendChannelListeners( - type: PubSubType, - channel: string, - listeners: ChannelListeners - ) { - if (!this.#extendChannelListeners(type, channel, listeners)) return; - - this.#isActive = true; - this.#subscribing++; - return { - args: [ - COMMANDS[type].subscribe, - channel - ], - channelsCounter: 1, - resolve: () => this.#subscribing--, - reject: () => { - this.#subscribing--; - this.#updateIsActive(); - } - }; + for (const listener of listeners.buffers) { + existingListeners.buffers.add(listener); } - #extendChannelListeners( - type: PubSubType, - channel: string, - listeners: ChannelListeners - ) { - const existingListeners = this.#listeners[type].get(channel); - if (!existingListeners) { - this.#listeners[type].set(channel, listeners); - return true; - } - - for (const listener of listeners.buffers) { - existingListeners.buffers.add(listener); - } - - for (const listener of listeners.strings) { - existingListeners.strings.add(listener); - } - - return false; + for (const listener of listeners.strings) { + existingListeners.strings.add(listener); } - extendTypeListeners(type: PubSubType, listeners: PubSubTypeListeners) { - const args: Array = [COMMANDS[type].subscribe]; - for (const [channel, channelListeners] of listeners) { - if (this.#extendChannelListeners(type, channel, channelListeners)) { - args.push(channel); - } - } + return false; + } - if (args.length === 1) return; - - this.#isActive = true; - this.#subscribing++; - return { - args, - channelsCounter: args.length - 1, - resolve: () => this.#subscribing--, - reject: () => { - this.#subscribing--; - this.#updateIsActive(); - } - }; + extendTypeListeners(type: PubSubType, listeners: PubSubTypeListeners) { + const args: Array = [COMMANDS[type].subscribe]; + for (const [channel, channelListeners] of listeners) { + if (this.#extendChannelListeners(type, channel, channelListeners)) { + args.push(channel); + } } - unsubscribe( - type: PubSubType, - channels?: string | Array, - listener?: PubSubListener, - returnBuffers?: T - ) { - const listeners = this.#listeners[type]; - if (!channels) { - return this.#unsubscribeCommand( - [COMMANDS[type].unsubscribe], - // cannot use `this.#subscribed` because there might be some `SUBSCRIBE` commands in the queue - // cannot use `this.#subscribed + this.#subscribing` because some `SUBSCRIBE` commands might fail - NaN, - () => listeners.clear() - ); - } + if (args.length === 1) return; - const channelsArray = PubSub.#channelsArray(channels); - if (!listener) { - return this.#unsubscribeCommand( - [COMMANDS[type].unsubscribe, ...channelsArray], - channelsArray.length, - () => { - for (const channel of channelsArray) { - listeners.delete(channel); - } - } - ); - } + this.#isActive = true; + this.#subscribing++; + return { + args, + channelsCounter: args.length - 1, + resolve: () => this.#subscribing--, + reject: () => { + this.#subscribing--; + this.#updateIsActive(); + } + } satisfies PubSubCommand; + } + + unsubscribe( + type: PubSubType, + channels?: string | Array, + listener?: PubSubListener, + returnBuffers?: T + ) { + const listeners = this.listeners[type]; + if (!channels) { + return this.#unsubscribeCommand( + [COMMANDS[type].unsubscribe], + // cannot use `this.#subscribed` because there might be some `SUBSCRIBE` commands in the queue + // cannot use `this.#subscribed + this.#subscribing` because some `SUBSCRIBE` commands might fail + NaN, + () => listeners.clear() + ); + } - const args: Array = [COMMANDS[type].unsubscribe]; - for (const channel of channelsArray) { - const sets = listeners.get(channel); - if (sets) { - let current, - other; - if (returnBuffers) { - current = sets.buffers; - other = sets.strings; - } else { - current = sets.strings; - other = sets.buffers; - } - - const currentSize = current.has(listener) ? current.size - 1 : current.size; - if (currentSize !== 0 || other.size !== 0) continue; - sets.unsubscribing = true; - } - - args.push(channel); + const channelsArray = PubSub.#channelsArray(channels); + if (!listener) { + return this.#unsubscribeCommand( + [COMMANDS[type].unsubscribe, ...channelsArray], + channelsArray.length, + () => { + for (const channel of channelsArray) { + listeners.delete(channel); + } } + ); + } - if (args.length === 1) { - // all channels has other listeners, - // delete the listeners without issuing a command - for (const channel of channelsArray) { - PubSub.#listenersSet( - listeners.get(channel)!, - returnBuffers - ).delete(listener); - } - return; + const args: Array = [COMMANDS[type].unsubscribe]; + for (const channel of channelsArray) { + const sets = listeners.get(channel); + if (sets) { + let current, + other; + if (returnBuffers) { + current = sets.buffers; + other = sets.strings; + } else { + current = sets.strings; + other = sets.buffers; } - return this.#unsubscribeCommand( - args, - args.length - 1, - () => { - for (const channel of channelsArray) { - const sets = listeners.get(channel); - if (!sets) continue; - - (returnBuffers ? sets.buffers : sets.strings).delete(listener); - if (sets.buffers.size === 0 && sets.strings.size === 0) { - listeners.delete(channel); - } - } - } - ); - } + const currentSize = current.has(listener) ? current.size - 1 : current.size; + if (currentSize !== 0 || other.size !== 0) continue; + sets.unsubscribing = true; + } - #unsubscribeCommand( - args: Array, - channelsCounter: number, - removeListeners: () => void - ) { - return { - args, - channelsCounter, - resolve: () => { - removeListeners(); - this.#updateIsActive(); - }, - reject: undefined // use the same structure as `subscribe` - }; + args.push(channel); } - #updateIsActive() { - this.#isActive = ( - this.#listeners[PubSubType.CHANNELS].size !== 0 || - this.#listeners[PubSubType.PATTERNS].size !== 0 || - this.#listeners[PubSubType.SHARDED].size !== 0 || - this.#subscribing !== 0 - ); + if (args.length === 1) { + // all channels has other listeners, + // delete the listeners without issuing a command + for (const channel of channelsArray) { + PubSub.#listenersSet( + listeners.get(channel)!, + returnBuffers + ).delete(listener); + } + return; } - reset() { - this.#isActive = false; - this.#subscribing = 0; - } + return this.#unsubscribeCommand( + args, + args.length - 1, + () => { + for (const channel of channelsArray) { + const sets = listeners.get(channel); + if (!sets) continue; - resubscribe(): Array { - const commands = []; - for (const [type, listeners] of Object.entries(this.#listeners)) { - if (!listeners.size) continue; - - this.#isActive = true; - this.#subscribing++; - const callback = () => this.#subscribing--; - commands.push({ - args: [ - COMMANDS[type as PubSubType].subscribe, - ...listeners.keys() - ], - channelsCounter: listeners.size, - resolve: callback, - reject: callback - }); + (returnBuffers ? sets.buffers : sets.strings).delete(listener); + if (sets.buffers.size === 0 && sets.strings.size === 0) { + listeners.delete(channel); + } } - - return commands; + } + ); + } + + #unsubscribeCommand( + args: Array, + channelsCounter: number, + removeListeners: () => void + ) { + return { + args, + channelsCounter, + resolve: () => { + removeListeners(); + this.#updateIsActive(); + }, + reject: undefined + } satisfies PubSubCommand; + } + + #updateIsActive() { + this.#isActive = ( + this.listeners[PUBSUB_TYPE.CHANNELS].size !== 0 || + this.listeners[PUBSUB_TYPE.PATTERNS].size !== 0 || + this.listeners[PUBSUB_TYPE.SHARDED].size !== 0 || + this.#subscribing !== 0 + ); + } + + reset() { + this.#isActive = false; + this.#subscribing = 0; + } + + resubscribe() { + const commands = []; + for (const [type, listeners] of Object.entries(this.listeners)) { + if (!listeners.size) continue; + + this.#isActive = true; + this.#subscribing++; + const callback = () => this.#subscribing--; + commands.push({ + args: [ + COMMANDS[type as PubSubType].subscribe, + ...listeners.keys() + ], + channelsCounter: listeners.size, + resolve: callback, + reject: callback + } satisfies PubSubCommand); } - handleMessageReply(reply: Array): boolean { - if (COMMANDS[PubSubType.CHANNELS].message.equals(reply[0])) { - this.#emitPubSubMessage( - PubSubType.CHANNELS, - reply[2], - reply[1] - ); - return true; - } else if (COMMANDS[PubSubType.PATTERNS].message.equals(reply[0])) { - this.#emitPubSubMessage( - PubSubType.PATTERNS, - reply[3], - reply[2], - reply[1] - ); - return true; - } else if (COMMANDS[PubSubType.SHARDED].message.equals(reply[0])) { - this.#emitPubSubMessage( - PubSubType.SHARDED, - reply[2], - reply[1] - ); - return true; - } - - return false; + return commands; + } + + handleMessageReply(reply: Array): boolean { + if (COMMANDS[PUBSUB_TYPE.CHANNELS].message.equals(reply[0])) { + this.#emitPubSubMessage( + PUBSUB_TYPE.CHANNELS, + reply[2], + reply[1] + ); + return true; + } else if (COMMANDS[PUBSUB_TYPE.PATTERNS].message.equals(reply[0])) { + this.#emitPubSubMessage( + PUBSUB_TYPE.PATTERNS, + reply[3], + reply[2], + reply[1] + ); + return true; + } else if (COMMANDS[PUBSUB_TYPE.SHARDED].message.equals(reply[0])) { + this.#emitPubSubMessage( + PUBSUB_TYPE.SHARDED, + reply[2], + reply[1] + ); + return true; } - removeShardedListeners(channel: string): ChannelListeners { - const listeners = this.#listeners[PubSubType.SHARDED].get(channel)!; - this.#listeners[PubSubType.SHARDED].delete(channel); - this.#updateIsActive(); - return listeners; + return false; + } + + removeShardedListeners(channel: string): ChannelListeners { + const listeners = this.listeners[PUBSUB_TYPE.SHARDED].get(channel)!; + this.listeners[PUBSUB_TYPE.SHARDED].delete(channel); + this.#updateIsActive(); + return listeners; + } + + #emitPubSubMessage( + type: PubSubType, + message: Buffer, + channel: Buffer, + pattern?: Buffer + ): void { + const keyString = (pattern ?? channel).toString(), + listeners = this.listeners[type].get(keyString); + + if (!listeners) return; + + for (const listener of listeners.buffers) { + listener(message, channel); } - - #emitPubSubMessage( - type: PubSubType, - message: Buffer, - channel: Buffer, - pattern?: Buffer - ): void { - const keyString = (pattern ?? channel).toString(), - listeners = this.#listeners[type].get(keyString); - - if (!listeners) return; - - for (const listener of listeners.buffers) { - listener(message, channel); - } - - if (!listeners.strings.size) return; - const channelString = pattern ? channel.toString() : keyString, - messageString = channelString === '__redis__:invalidate' ? - // https://github.com/redis/redis/pull/7469 - // https://github.com/redis/redis/issues/7463 - (message === null ? null : (message as any as Array).map(x => x.toString())) as any : - message.toString(); - for (const listener of listeners.strings) { - listener(messageString, channelString); - } - } + if (!listeners.strings.size) return; - getTypeListeners(type: PubSubType): PubSubTypeListeners { - return this.#listeners[type]; + const channelString = pattern ? channel.toString() : keyString, + messageString = channelString === '__redis__:invalidate' ? + // https://github.com/redis/redis/pull/7469 + // https://github.com/redis/redis/issues/7463 + (message === null ? null : (message as any as Array).map(x => x.toString())) as any : + message.toString(); + for (const listener of listeners.strings) { + listener(messageString, channelString); } + } } diff --git a/packages/client/lib/client/socket.spec.ts b/packages/client/lib/client/socket.spec.ts index eb555351ac4..20b238a3a38 100644 --- a/packages/client/lib/client/socket.spec.ts +++ b/packages/client/lib/client/socket.spec.ts @@ -1,87 +1,87 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import { spy } from 'sinon'; -import { once } from 'events'; +import { once } from 'node:events'; import RedisSocket, { RedisSocketOptions } from './socket'; describe('Socket', () => { - function createSocket(options: RedisSocketOptions): RedisSocket { - const socket = new RedisSocket( - () => Promise.resolve(), - options - ); - - socket.on('error', () => { - // ignore errors - }); - - return socket; - } - - describe('reconnectStrategy', () => { - it('false', async () => { - const socket = createSocket({ - host: 'error', - connectTimeout: 1, - reconnectStrategy: false - }); - - await assert.rejects(socket.connect()); - - assert.equal(socket.isOpen, false); - }); - - it('0', async () => { - const socket = createSocket({ - host: 'error', - connectTimeout: 1, - reconnectStrategy: 0 - }); - - socket.connect(); - await once(socket, 'error'); - assert.equal(socket.isOpen, true); - assert.equal(socket.isReady, false); - socket.disconnect(); - assert.equal(socket.isOpen, false); - }); - - it('custom strategy', async () => { - const numberOfRetries = 3; - - const reconnectStrategy = spy((retries: number) => { - assert.equal(retries + 1, reconnectStrategy.callCount); - - if (retries === numberOfRetries) return new Error(`${numberOfRetries}`); - - return 0; - }); - - const socket = createSocket({ - host: 'error', - connectTimeout: 1, - reconnectStrategy - }); - - await assert.rejects(socket.connect(), { - message: `${numberOfRetries}` - }); - - assert.equal(socket.isOpen, false); - }); - - it('should handle errors', async () => { - const socket = createSocket({ - host: 'error', - connectTimeout: 1, - reconnectStrategy(retries: number) { - if (retries === 1) return new Error('done'); - throw new Error(); - } - }); - - await assert.rejects(socket.connect()); - - assert.equal(socket.isOpen, false); - }); + function createSocket(options: RedisSocketOptions): RedisSocket { + const socket = new RedisSocket( + () => Promise.resolve(), + options + ); + + socket.on('error', () => { + // ignore errors }); + + return socket; + } + + describe('reconnectStrategy', () => { + it('false', async () => { + const socket = createSocket({ + host: 'error', + connectTimeout: 1, + reconnectStrategy: false + }); + + await assert.rejects(socket.connect()); + + assert.equal(socket.isOpen, false); + }); + + it('0', async () => { + const socket = createSocket({ + host: 'error', + connectTimeout: 1, + reconnectStrategy: 0 + }); + + socket.connect(); + await once(socket, 'error'); + assert.equal(socket.isOpen, true); + assert.equal(socket.isReady, false); + socket.destroy(); + assert.equal(socket.isOpen, false); + }); + + it('custom strategy', async () => { + const numberOfRetries = 3; + + const reconnectStrategy = spy((retries: number) => { + assert.equal(retries + 1, reconnectStrategy.callCount); + + if (retries === numberOfRetries) return new Error(`${numberOfRetries}`); + + return 0; + }); + + const socket = createSocket({ + host: 'error', + connectTimeout: 1, + reconnectStrategy + }); + + await assert.rejects(socket.connect(), { + message: `${numberOfRetries}` + }); + + assert.equal(socket.isOpen, false); + }); + + it('should handle errors', async () => { + const socket = createSocket({ + host: 'error', + connectTimeout: 1, + reconnectStrategy(retries: number) { + if (retries === 1) return new Error('done'); + throw new Error(); + } + }); + + await assert.rejects(socket.connect()); + + assert.equal(socket.isOpen, false); + }); + }); }); diff --git a/packages/client/lib/client/socket.ts b/packages/client/lib/client/socket.ts index b701f6ea979..3c2666e1067 100644 --- a/packages/client/lib/client/socket.ts +++ b/packages/client/lib/client/socket.ts @@ -1,310 +1,345 @@ -import { EventEmitter } from 'events'; -import * as net from 'net'; -import * as tls from 'tls'; -import { RedisCommandArguments } from '../commands'; +import { EventEmitter, once } from 'node:events'; +import net from 'node:net'; +import tls from 'node:tls'; import { ConnectionTimeoutError, ClientClosedError, SocketClosedUnexpectedlyError, ReconnectStrategyError } from '../errors'; -import { promiseTimeout } from '../utils'; - -export interface RedisSocketCommonOptions { - /** - * Connection Timeout (in milliseconds) - */ - connectTimeout?: number; - /** - * Toggle [`Nagle's algorithm`](https://nodejs.org/api/net.html#net_socket_setnodelay_nodelay) - */ - noDelay?: boolean; - /** - * Toggle [`keep-alive`](https://nodejs.org/api/net.html#net_socket_setkeepalive_enable_initialdelay) - */ - keepAlive?: number | false; - /** - * When the socket closes unexpectedly (without calling `.quit()`/`.disconnect()`), the client uses `reconnectStrategy` to decide what to do. The following values are supported: - * 1. `false` -> do not reconnect, close the client and flush the command queue. - * 2. `number` -> wait for `X` milliseconds before reconnecting. - * 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error. - * Defaults to `retries => Math.min(retries * 50, 500)` - */ - reconnectStrategy?: false | number | ((retries: number, cause: Error) => false | Error | number); -} +import { setTimeout } from 'node:timers/promises'; +import { RedisArgument } from '../RESP/types'; -type RedisNetSocketOptions = Partial & { - tls?: false; +type NetOptions = { + tls?: false; }; -export interface RedisTlsSocketOptions extends tls.ConnectionOptions { - tls: true; +type ReconnectStrategyFunction = (retries: number, cause: Error) => false | Error | number; + +type RedisSocketOptionsCommon = { + /** + * Connection timeout (in milliseconds) + */ + connectTimeout?: number; + /** + * When the socket closes unexpectedly (without calling `.close()`/`.destroy()`), the client uses `reconnectStrategy` to decide what to do. The following values are supported: + * 1. `false` -> do not reconnect, close the client and flush the command queue. + * 2. `number` -> wait for `X` milliseconds before reconnecting. + * 3. `(retries: number, cause: Error) => false | number | Error` -> `number` is the same as configuring a `number` directly, `Error` is the same as `false`, but with a custom error. + */ + reconnectStrategy?: false | number | ReconnectStrategyFunction; } -export type RedisSocketOptions = RedisSocketCommonOptions & (RedisNetSocketOptions | RedisTlsSocketOptions); +type RedisTcpOptions = RedisSocketOptionsCommon & NetOptions & Omit< + net.TcpNetConnectOpts, + 'timeout' | 'onread' | 'readable' | 'writable' | 'port' +> & { + port?: number; +}; -interface CreateSocketReturn { - connectEvent: string; - socket: T; +type RedisTlsOptions = RedisSocketOptionsCommon & tls.ConnectionOptions & { + tls: true; + host: string; } -export type RedisSocketInitiator = () => Promise; - -export default class RedisSocket extends EventEmitter { - static #initiateOptions(options?: RedisSocketOptions): RedisSocketOptions { - options ??= {}; - if (!(options as net.IpcSocketConnectOpts).path) { - (options as net.TcpSocketConnectOpts).port ??= 6379; - (options as net.TcpSocketConnectOpts).host ??= 'localhost'; - } - - options.connectTimeout ??= 5000; - options.keepAlive ??= 5000; - options.noDelay ??= true; - - return options; - } +type RedisIpcOptions = RedisSocketOptionsCommon & Omit< + net.IpcNetConnectOpts, + 'timeout' | 'onread' | 'readable' | 'writable' +> & { + tls: false; +} - static #isTlsSocket(options: RedisSocketOptions): options is RedisTlsSocketOptions { - return (options as RedisTlsSocketOptions).tls === true; - } +export type RedisTcpSocketOptions = RedisTcpOptions | RedisTlsOptions; - readonly #initiator: RedisSocketInitiator; +export type RedisSocketOptions = RedisTcpSocketOptions | RedisIpcOptions; - readonly #options: RedisSocketOptions; +export type RedisSocketInitiator = () => void | Promise; - #socket?: net.Socket | tls.TLSSocket; +export default class RedisSocket extends EventEmitter { + readonly #initiator; + readonly #connectTimeout; + readonly #reconnectStrategy; + readonly #socketFactory; - #isOpen = false; + #socket?: net.Socket | tls.TLSSocket; - get isOpen(): boolean { - return this.#isOpen; - } + #isOpen = false; - #isReady = false; + get isOpen() { + return this.#isOpen; + } - get isReady(): boolean { - return this.#isReady; - } + #isReady = false; - // `writable.writableNeedDrain` was added in v15.2.0 and therefore can't be used - // https://nodejs.org/api/stream.html#stream_writable_writableneeddrain - #writableNeedDrain = false; + get isReady() { + return this.#isReady; + } - get writableNeedDrain(): boolean { - return this.#writableNeedDrain; - } + #isSocketUnrefed = false; - #isSocketUnrefed = false; + constructor(initiator: RedisSocketInitiator, options?: RedisSocketOptions) { + super(); - constructor(initiator: RedisSocketInitiator, options?: RedisSocketOptions) { - super(); + this.#initiator = initiator; + this.#connectTimeout = options?.connectTimeout ?? 5000; + this.#reconnectStrategy = this.#createReconnectStrategy(options); + this.#socketFactory = this.#createSocketFactory(options); + } - this.#initiator = initiator; - this.#options = RedisSocket.#initiateOptions(options); + #createReconnectStrategy(options?: RedisSocketOptions): ReconnectStrategyFunction { + const strategy = options?.reconnectStrategy; + if (strategy === false || typeof strategy === 'number') { + return () => strategy; } - #reconnectStrategy(retries: number, cause: Error) { - if (this.#options.reconnectStrategy === false) { - return false; - } else if (typeof this.#options.reconnectStrategy === 'number') { - return this.#options.reconnectStrategy; - } else if (this.#options.reconnectStrategy) { - try { - const retryIn = this.#options.reconnectStrategy(retries, cause); - if (retryIn !== false && !(retryIn instanceof Error) && typeof retryIn !== 'number') { - throw new TypeError(`Reconnect strategy should return \`false | Error | number\`, got ${retryIn} instead`); - } - - return retryIn; - } catch (err) { - this.emit('error', err); - } + if (strategy) { + return (retries, cause) => { + try { + const retryIn = strategy(retries, cause); + if (retryIn !== false && !(retryIn instanceof Error) && typeof retryIn !== 'number') { + throw new TypeError(`Reconnect strategy should return \`false | Error | number\`, got ${retryIn} instead`); + } + return retryIn; + } catch (err) { + this.emit('error', err); + return this.defaultReconnectStrategy(retries); } - - return Math.min(retries * 50, 500); + }; } - #shouldReconnect(retries: number, cause: Error) { - const retryIn = this.#reconnectStrategy(retries, cause); - if (retryIn === false) { - this.#isOpen = false; - this.emit('error', cause); - return cause; - } else if (retryIn instanceof Error) { - this.#isOpen = false; - this.emit('error', cause); - return new ReconnectStrategyError(retryIn, cause); - } - - return retryIn; + return this.defaultReconnectStrategy; + } + + #createSocketFactory(options?: RedisSocketOptions) { + // TLS + if (options?.tls === true) { + const withDefaults: tls.ConnectionOptions = { + ...options, + port: options?.port ?? 6379, + // https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed" + // @types/node is... incorrect... + // @ts-expect-error + noDelay: options?.noDelay ?? true, + // https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed" + // @types/node is... incorrect... + // @ts-expect-error + keepAlive: options?.keepAlive ?? true, + // https://nodejs.org/api/tls.html#tlsconnectoptions-callback "Any socket.connect() option not already listed" + // @types/node is... incorrect... + // @ts-expect-error + keepAliveInitialDelay: options?.keepAliveInitialDelay ?? 5000, + timeout: undefined, + onread: undefined, + readable: true, + writable: true + }; + return { + create() { + return tls.connect(withDefaults); + }, + event: 'secureConnect' + }; } - async connect(): Promise { - if (this.#isOpen) { - throw new Error('Socket already opened'); - } - - this.#isOpen = true; - return this.#connect(); + // IPC + if (options && 'path' in options) { + const withDefaults: net.IpcNetConnectOpts = { + ...options, + timeout: undefined, + onread: undefined, + readable: true, + writable: true + }; + return { + create() { + return net.createConnection(withDefaults); + }, + event: 'connect' + }; } - async #connect(): Promise { - let retries = 0; - do { - try { - this.#socket = await this.#createSocket(); - this.#writableNeedDrain = false; - this.emit('connect'); - - try { - await this.#initiator(); - } catch (err) { - this.#socket.destroy(); - this.#socket = undefined; - throw err; - } - this.#isReady = true; - this.emit('ready'); - } catch (err) { - const retryIn = this.#shouldReconnect(retries++, err as Error); - if (typeof retryIn !== 'number') { - throw retryIn; - } - - this.emit('error', err); - await promiseTimeout(retryIn); - this.emit('reconnecting'); - } - } while (this.#isOpen && !this.#isReady); + // TCP + const withDefaults: net.TcpNetConnectOpts = { + ...options, + port: options?.port ?? 6379, + noDelay: options?.noDelay ?? true, + keepAlive: options?.keepAlive ?? true, + keepAliveInitialDelay: options?.keepAliveInitialDelay ?? 5000, + timeout: undefined, + onread: undefined, + readable: true, + writable: true + }; + return { + create() { + return net.createConnection(withDefaults); + }, + event: 'connect' + }; + } + + #shouldReconnect(retries: number, cause: Error) { + const retryIn = this.#reconnectStrategy(retries, cause); + if (retryIn === false) { + this.#isOpen = false; + this.emit('error', cause); + return cause; + } else if (retryIn instanceof Error) { + this.#isOpen = false; + this.emit('error', cause); + return new ReconnectStrategyError(retryIn, cause); } - #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())); - } - - if (this.#isSocketUnrefed) { - socket.unref(); - } - - socket - .setNoDelay(this.#options.noDelay) - .once('error', reject) - .once(connectEvent, () => { - socket - .setTimeout(0) - // https://github.com/nodejs/node/issues/31663 - .setKeepAlive(this.#options.keepAlive !== false, this.#options.keepAlive || 0) - .off('error', reject) - .once('error', (err: Error) => this.#onSocketError(err)) - .once('close', hadError => { - if (!hadError && this.#isOpen && this.#socket === socket) { - this.#onSocketError(new SocketClosedUnexpectedlyError()); - } - }) - .on('drain', () => { - this.#writableNeedDrain = false; - this.emit('drain'); - }) - .on('data', data => this.emit('data', data)); - - resolve(socket); - }); - }); - } + return retryIn; + } - #createNetSocket(): CreateSocketReturn { - return { - connectEvent: 'connect', - socket: net.connect(this.#options as net.NetConnectOpts) // TODO - }; + async connect(): Promise { + if (this.#isOpen) { + throw new Error('Socket already opened'); } - #createTlsSocket(): CreateSocketReturn { - return { - connectEvent: 'secureConnect', - socket: tls.connect(this.#options as tls.ConnectionOptions) // TODO - }; - } + this.#isOpen = true; + return this.#connect(); + } + + async #connect(): Promise { + let retries = 0; + do { + try { + this.#socket = await this.#createSocket(); + this.emit('connect'); + + try { + await this.#initiator(); + } catch (err) { + this.#socket.destroy(); + this.#socket = undefined; + throw err; + } + this.#isReady = true; + this.emit('ready'); + } catch (err) { + const retryIn = this.#shouldReconnect(retries++, err as Error); + if (typeof retryIn !== 'number') { + throw retryIn; + } - #onSocketError(err: Error): void { - const wasReady = this.#isReady; - this.#isReady = false; this.emit('error', err); - - if (!wasReady || !this.#isOpen || typeof this.#shouldReconnect(0, err) !== 'number') return; - + await setTimeout(retryIn); this.emit('reconnecting'); - this.#connect().catch(() => { - // the error was already emitted, silently ignore it - }); + } + } while (this.#isOpen && !this.#isReady); + } + + async #createSocket(): Promise { + const socket = this.#socketFactory.create(); + + let onTimeout; + if (this.#connectTimeout !== undefined) { + onTimeout = () => socket.destroy(new ConnectionTimeoutError()); + socket.once('timeout', onTimeout); + socket.setTimeout(this.#connectTimeout); } - writeCommand(args: RedisCommandArguments): void { - if (!this.#socket) { - throw new ClientClosedError(); - } - - for (const toWrite of args) { - this.#writableNeedDrain = !this.#socket.write(toWrite); - } + if (this.#isSocketUnrefed) { + socket.unref(); } - disconnect(): void { - if (!this.#isOpen) { - throw new ClientClosedError(); - } + await once(socket, this.#socketFactory.event); - this.#isOpen = false; - this.#disconnect(); + if (onTimeout) { + socket.removeListener('timeout', onTimeout); } - #disconnect(): void { - this.#isReady = false; + socket + .once('error', err => this.#onSocketError(err)) + .once('close', hadError => { + if (hadError || !this.#isOpen || this.#socket !== socket) return; + this.#onSocketError(new SocketClosedUnexpectedlyError()); + }) + .on('drain', () => this.emit('drain')) + .on('data', data => this.emit('data', data)); + + return socket; + } + + #onSocketError(err: Error): void { + const wasReady = this.#isReady; + this.#isReady = false; + this.emit('error', err); + + if (!wasReady || !this.#isOpen || typeof this.#shouldReconnect(0, err) !== 'number') return; + + this.emit('reconnecting'); + this.#connect().catch(() => { + // the error was already emitted, silently ignore it + }); + } + + write(iterable: Iterable>) { + if (!this.#socket) return; + + this.#socket.cork(); + for (const args of iterable) { + for (const toWrite of args) { + this.#socket.write(toWrite); + } + + if (this.#socket.writableNeedDrain) break; + } + this.#socket.uncork(); + } - if (this.#socket) { - this.#socket.destroy(); - this.#socket = undefined; - } - - this.emit('end'); + async quit(fn: () => Promise): Promise { + if (!this.#isOpen) { + throw new ClientClosedError(); } - async quit(fn: () => Promise): Promise { - if (!this.#isOpen) { - throw new ClientClosedError(); - } + this.#isOpen = false; + const reply = await fn(); + this.destroySocket(); + return reply; + } - this.#isOpen = false; - const reply = await fn(); - this.#disconnect(); - return reply; + close() { + if (!this.#isOpen) { + throw new ClientClosedError(); } - #isCorked = false; + this.#isOpen = false; + } - cork(): void { - if (!this.#socket || this.#isCorked) { - return; - } + destroy() { + if (!this.#isOpen) { + throw new ClientClosedError(); + } - this.#socket.cork(); - this.#isCorked = true; + this.#isOpen = false; + this.destroySocket(); + } - setImmediate(() => { - this.#socket?.uncork(); - this.#isCorked = false; - }); - } + destroySocket() { + this.#isReady = false; - ref(): void { - this.#isSocketUnrefed = false; - this.#socket?.ref(); + if (this.#socket) { + this.#socket.destroy(); + this.#socket = undefined; } - unref(): void { - this.#isSocketUnrefed = true; - this.#socket?.unref(); - } + this.emit('end'); + } + + ref() { + this.#isSocketUnrefed = false; + this.#socket?.ref(); + } + + unref() { + this.#isSocketUnrefed = true; + this.#socket?.unref(); + } + + defaultReconnectStrategy(retries: number) { + // Generate a random jitter between 0 – 200 ms: + const jitter = Math.floor(Math.random() * 200); + // Delay is an exponential back off, (times^2) * 50 ms, with a maximum value of 2000 ms: + const delay = Math.min(Math.pow(2, retries) * 50, 2000); + + return delay + jitter; + } } diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 45c96a80b50..824cf2ae813 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -1,621 +1,614 @@ -import RedisClient, { InstantiableRedisClient, RedisClientType } from '../client'; import { RedisClusterClientOptions, RedisClusterOptions } from '.'; -import { RedisCommandArgument, RedisFunctions, RedisModules, RedisScripts } from '../commands'; import { RootNodesUnavailableError } from '../errors'; -import { ClusterSlotsNode } from '../commands/CLUSTER_SLOTS'; -import { types } from 'util'; -import { ChannelListeners, PubSubType, PubSubTypeListeners } from '../client/pub-sub'; -import { EventEmitter } from 'stream'; - -// We need to use 'require', because it's not possible with Typescript to import -// function that are exported as 'module.exports = function`, without esModuleInterop -// set to true. -const calculateSlot = require('cluster-key-slot'); +import RedisClient, { RedisClientOptions, RedisClientType } from '../client'; +import { EventEmitter } from 'node:stream'; +import { ChannelListeners, PUBSUB_TYPE, PubSubTypeListeners } from '../client/pub-sub'; +import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; +import calculateSlot from 'cluster-key-slot'; +import { RedisSocketOptions } from '../client/socket'; interface NodeAddress { - host: string; - port: number; + host: string; + port: number; } export type NodeAddressMap = { - [address: string]: NodeAddress; + [address: string]: NodeAddress; } | ((address: string) => NodeAddress | undefined); -type ValueOrPromise = T | Promise; - -type ClientOrPromise< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = ValueOrPromise>; - export interface Node< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > { - address: string; - client?: ClientOrPromise; + address: string; + client?: RedisClientType; + connectPromise?: Promise>; } export interface ShardNode< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> extends Node { - id: string; - host: string; - port: number; - readonly: boolean; + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> extends Node, NodeAddress { + id: string; + readonly: boolean; } export interface MasterNode< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> extends ShardNode { - pubSubClient?: ClientOrPromise; + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> extends ShardNode { + pubSub?: { + connectPromise?: Promise>; + client: RedisClientType; + }; } export interface Shard< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > { - master: MasterNode; - replicas?: Array>; - nodesIterator?: IterableIterator>; + master: MasterNode; + replicas?: Array>; + nodesIterator?: IterableIterator>; } type ShardWithReplicas< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = Shard & Required, 'replicas'>>; - -export type PubSubNode< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = Required>; + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = Shard & Required, 'replicas'>>; + +type PubSubNode< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = ( + Omit, 'client'> & + Required, 'client'>> +); type PubSubToResubscribe = Record< - PubSubType.CHANNELS | PubSubType.PATTERNS, - PubSubTypeListeners + PUBSUB_TYPE['CHANNELS'] | PUBSUB_TYPE['PATTERNS'], + PubSubTypeListeners >; export type OnShardedChannelMovedError = ( - err: unknown, - channel: string, - listeners?: ChannelListeners + err: unknown, + channel: string, + listeners?: ChannelListeners ) => void; export default class RedisClusterSlots< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > { - static #SLOTS = 16384; - - readonly #options: RedisClusterOptions; - readonly #Client: InstantiableRedisClient; - readonly #emit: EventEmitter['emit']; - slots = new Array>(RedisClusterSlots.#SLOTS); - shards = new Array>(); - masters = new Array>(); - replicas = new Array>(); - readonly nodeByAddress = new Map | ShardNode>(); - pubSubNode?: PubSubNode; - - #isOpen = false; - - get isOpen() { - return this.#isOpen; - } - - constructor( - options: RedisClusterOptions, - emit: EventEmitter['emit'] - ) { - this.#options = options; - this.#Client = RedisClient.extend(options); - this.#emit = emit; + static #SLOTS = 16384; + + readonly #options; + readonly #clientFactory; + readonly #emit: EventEmitter['emit']; + slots = new Array>(RedisClusterSlots.#SLOTS); + masters = new Array>(); + replicas = new Array>(); + readonly nodeByAddress = new Map | ShardNode>(); + pubSubNode?: PubSubNode; + + #isOpen = false; + + get isOpen() { + return this.#isOpen; + } + + constructor( + options: RedisClusterOptions, + emit: EventEmitter['emit'] + ) { + this.#options = options; + this.#clientFactory = RedisClient.factory(options); + this.#emit = emit; + } + + async connect() { + if (this.#isOpen) { + throw new Error('Cluster already open'); } - async connect() { - if (this.#isOpen) { - throw new Error('Cluster already open'); - } - - this.#isOpen = true; - try { - await this.#discoverWithRootNodes(); - } catch (err) { - this.#isOpen = false; - throw err; - } + this.#isOpen = true; + try { + await this.#discoverWithRootNodes(); + } catch (err) { + this.#isOpen = false; + throw err; } + } - async #discoverWithRootNodes() { - let start = Math.floor(Math.random() * this.#options.rootNodes.length); - for (let i = start; i < this.#options.rootNodes.length; i++) { - if (await this.#discover(this.#options.rootNodes[i])) return; - } - - for (let i = 0; i < start; i++) { - if (await this.#discover(this.#options.rootNodes[i])) return; - } - - throw new RootNodesUnavailableError(); + async #discoverWithRootNodes() { + let start = Math.floor(Math.random() * this.#options.rootNodes.length); + for (let i = start; i < this.#options.rootNodes.length; i++) { + if (!this.#isOpen) throw new Error('Cluster closed'); + if (await this.#discover(this.#options.rootNodes[i])) return; } - #resetSlots() { - this.slots = new Array(RedisClusterSlots.#SLOTS); - this.shards = []; - this.masters = []; - this.replicas = []; - this.#randomNodeIterator = undefined; + for (let i = 0; i < start; i++) { + if (!this.#isOpen) throw new Error('Cluster closed'); + if (await this.#discover(this.#options.rootNodes[i])) return; } - async #discover(rootNode?: RedisClusterClientOptions) { - const addressesInUse = new Set(); + throw new RootNodesUnavailableError(); + } + + #resetSlots() { + this.slots = new Array(RedisClusterSlots.#SLOTS); + this.masters = []; + this.replicas = []; + this._randomNodeIterator = undefined; + } + + async #discover(rootNode: RedisClusterClientOptions) { + this.#resetSlots(); + try { + const addressesInUse = new Set(), + promises: Array> = [], + eagerConnect = this.#options.minimizeConnections !== true; + + for (const { from, to, master, replicas } of await this.#getShards(rootNode)) { + const shard: Shard = { + master: this.#initiateSlotNode(master, false, eagerConnect, addressesInUse, promises) + }; - try { - const shards = await this.#getShards(rootNode), - promises: Array> = [], - eagerConnect = this.#options.minimizeConnections !== true; - this.#resetSlots(); - for (const { from, to, master, replicas } of shards) { - const shard: Shard = { - master: this.#initiateSlotNode(master, false, eagerConnect, addressesInUse, promises) - }; - - if (this.#options.useReplicas) { - shard.replicas = replicas.map(replica => - this.#initiateSlotNode(replica, true, eagerConnect, addressesInUse, promises) - ); - } - - this.shards.push(shard); - - for (let i = from; i <= to; i++) { - this.slots[i] = shard; - } - } - - if (this.pubSubNode && !addressesInUse.has(this.pubSubNode.address)) { - if (types.isPromise(this.pubSubNode.client)) { - promises.push( - this.pubSubNode.client.then(client => client.disconnect()) - ); - this.pubSubNode = undefined; - } else { - promises.push(this.pubSubNode.client.disconnect()); - - const channelsListeners = this.pubSubNode.client.getPubSubListeners(PubSubType.CHANNELS), - patternsListeners = this.pubSubNode.client.getPubSubListeners(PubSubType.PATTERNS); - - if (channelsListeners.size || patternsListeners.size) { - promises.push( - this.#initiatePubSubClient({ - [PubSubType.CHANNELS]: channelsListeners, - [PubSubType.PATTERNS]: patternsListeners - }) - ); - } - } - } - - for (const [address, node] of this.nodeByAddress.entries()) { - if (addressesInUse.has(address)) continue; - - if (node.client) { - promises.push( - this.#execOnNodeClient(node.client, client => client.disconnect()) - ); - } - - const { pubSubClient } = node as MasterNode; - if (pubSubClient) { - promises.push( - this.#execOnNodeClient(pubSubClient, client => client.disconnect()) - ); - } - - this.nodeByAddress.delete(address); - } - - await Promise.all(promises); - - return true; - } catch (err) { - this.#emit('error', err); - return false; + if (this.#options.useReplicas) { + shard.replicas = replicas.map(replica => + this.#initiateSlotNode(replica, true, eagerConnect, addressesInUse, promises) + ); } - } - async #getShards(rootNode?: RedisClusterClientOptions) { - const client = new this.#Client( - this.#clientOptionsDefaults(rootNode, true) - ); + for (let i = from; i <= to; i++) { + this.slots[i] = shard; + } + } - client.on('error', err => this.#emit('error', err)); + if (this.pubSubNode && !addressesInUse.has(this.pubSubNode.address)) { + const channelsListeners = this.pubSubNode.client.getPubSubListeners(PUBSUB_TYPE.CHANNELS), + patternsListeners = this.pubSubNode.client.getPubSubListeners(PUBSUB_TYPE.PATTERNS); - await client.connect(); + this.pubSubNode.client.destroy(); - try { - // using `CLUSTER SLOTS` and not `CLUSTER SHARDS` to support older versions - return await client.clusterSlots(); - } finally { - await client.disconnect(); + if (channelsListeners.size || patternsListeners.size) { + promises.push( + this.#initiatePubSubClient({ + [PUBSUB_TYPE.CHANNELS]: channelsListeners, + [PUBSUB_TYPE.PATTERNS]: patternsListeners + }) + ); } - } + } - #getNodeAddress(address: string): NodeAddress | undefined { - switch (typeof this.#options.nodeAddressMap) { - case 'object': - return this.#options.nodeAddressMap[address]; + for (const [address, node] of this.nodeByAddress.entries()) { + if (addressesInUse.has(address)) continue; - case 'function': - return this.#options.nodeAddressMap(address); + if (node.client) { + node.client.destroy(); } - } - #clientOptionsDefaults( - options?: RedisClusterClientOptions, - disableReconnect?: boolean - ): RedisClusterClientOptions | undefined { - let result: RedisClusterClientOptions | undefined; - if (this.#options.defaults) { - let socket; - if (this.#options.defaults.socket) { - socket = { - ...this.#options.defaults.socket, - ...options?.socket - }; - } else { - socket = options?.socket; - } - - result = { - ...this.#options.defaults, - ...options, - socket - }; - } else { - result = options; - } - - if (disableReconnect) { - result ??= {}; - result.socket ??= {}; - result.socket.reconnectStrategy = false; + const { pubSub } = node as MasterNode; + if (pubSub) { + pubSub.client.destroy(); } - return result; - } - - #initiateSlotNode( - { id, ip, port }: ClusterSlotsNode, - readonly: boolean, - eagerConnent: boolean, - addressesInUse: Set, - promises: Array> - ) { - const address = `${ip}:${port}`; - addressesInUse.add(address); - - let node = this.nodeByAddress.get(address); - if (!node) { - node = { - id, - host: ip, - port, - address, - readonly, - client: undefined - }; - - if (eagerConnent) { - promises.push(this.#createNodeClient(node)); - } - - this.nodeByAddress.set(address, node); - } + this.nodeByAddress.delete(address); + } - (readonly ? this.replicas : this.masters).push(node); + await Promise.all(promises); - return node; + return true; + } catch (err) { + this.#emit('error', err); + return false; } - - async #createClient( - node: ShardNode, - readonly = node.readonly - ) { - const client = new this.#Client( - this.#clientOptionsDefaults({ - socket: this.#getNodeAddress(node.address) ?? { - host: node.host, - port: node.port - }, - readonly - }) - ); - client.on('error', err => this.#emit('error', err)); - - await client.connect(); - - return client; + } + + async #getShards(rootNode: RedisClusterClientOptions) { + const options = this.#clientOptionsDefaults(rootNode)!; + options.socket ??= {}; + options.socket.reconnectStrategy = false; + options.RESP = this.#options.RESP; + options.commandOptions = undefined; + + // TODO: find a way to avoid type casting + const client = await this.#clientFactory(options as RedisClientOptions) + .on('error', err => this.#emit('error', err)) + .connect(); + + try { + // switch to `CLUSTER SHARDS` when Redis 7.0 will be the minimum supported version + return await client.clusterSlots(); + } finally { + client.destroy(); } + } - #createNodeClient(node: ShardNode) { - const promise = this.#createClient(node) - .then(client => { - node.client = client; - return client; - }) - .catch(err => { - node.client = undefined; - throw err; - }); - node.client = promise; - return promise; - } + #getNodeAddress(address: string): NodeAddress | undefined { + switch (typeof this.#options.nodeAddressMap) { + case 'object': + return this.#options.nodeAddressMap[address]; - nodeClient(node: ShardNode) { - return node.client ?? this.#createNodeClient(node); + case 'function': + return this.#options.nodeAddressMap(address); } - - #runningRediscoverPromise?: Promise; - - async rediscover(startWith: RedisClientType): Promise { - this.#runningRediscoverPromise ??= this.#rediscover(startWith) - .finally(() => this.#runningRediscoverPromise = undefined); - return this.#runningRediscoverPromise; + } + + #clientOptionsDefaults(options?: RedisClientOptions) { + if (!this.#options.defaults) return options; + + let socket; + if (this.#options.defaults.socket) { + socket = options?.socket ? { + ...this.#options.defaults.socket, + ...options.socket + } : this.#options.defaults.socket; + } else { + socket = options?.socket; } - async #rediscover(startWith: RedisClientType): Promise { - if (await this.#discover(startWith.options)) return; - - return this.#discoverWithRootNodes(); + return { + ...this.#options.defaults, + ...options, + socket: socket as RedisSocketOptions + }; + } + + #initiateSlotNode( + shard: NodeAddress & { id: string; }, + readonly: boolean, + eagerConnent: boolean, + addressesInUse: Set, + promises: Array> + ) { + const address = `${shard.host}:${shard.port}`; + + let node = this.nodeByAddress.get(address); + if (!node) { + node = { + ...shard, + address, + readonly, + client: undefined, + connectPromise: undefined + }; + + if (eagerConnent) { + promises.push(this.#createNodeClient(node)); + } + + this.nodeByAddress.set(address, node); } - quit(): Promise { - return this.#destroy(client => client.quit()); + if (!addressesInUse.has(address)) { + addressesInUse.add(address); + (readonly ? this.replicas : this.masters).push(node); } - disconnect(): Promise { - return this.#destroy(client => client.disconnect()); + return node; + } + + #createClient(node: ShardNode, readonly = node.readonly) { + return this.#clientFactory( + this.#clientOptionsDefaults({ + socket: this.#getNodeAddress(node.address) ?? { + host: node.host, + port: node.port + }, + readonly + }) + ).on('error', err => console.error(err)); + } + + #createNodeClient(node: ShardNode, readonly?: boolean) { + const client = node.client = this.#createClient(node, readonly); + return node.connectPromise = client.connect() + .finally(() => node.connectPromise = undefined); + } + + nodeClient(node: ShardNode) { + return ( + node.connectPromise ?? // if the node is connecting + node.client ?? // if the node is connected + this.#createNodeClient(node) // if the not is disconnected + ); + } + + #runningRediscoverPromise?: Promise; + + async rediscover(startWith: RedisClientType): Promise { + this.#runningRediscoverPromise ??= this.#rediscover(startWith) + .finally(() => this.#runningRediscoverPromise = undefined); + return this.#runningRediscoverPromise; + } + + async #rediscover(startWith: RedisClientType): Promise { + if (await this.#discover(startWith.options!)) return; + + return this.#discoverWithRootNodes(); + } + + /** + * @deprecated Use `close` instead. + */ + quit(): Promise { + return this.#destroy(client => client.quit()); + } + + /** + * @deprecated Use `destroy` instead. + */ + disconnect(): Promise { + return this.#destroy(client => client.disconnect()); + } + + close() { + return this.#destroy(client => client.close()); + } + + destroy() { + this.#isOpen = false; + + for (const client of this.#clients()) { + client.destroy(); } - async #destroy(fn: (client: RedisClientType) => Promise): Promise { - this.#isOpen = false; - - const promises = []; - for (const { master, replicas } of this.shards) { - if (master.client) { - promises.push( - this.#execOnNodeClient(master.client, fn) - ); - } - - if (master.pubSubClient) { - promises.push( - this.#execOnNodeClient(master.pubSubClient, fn) - ); - } - - if (replicas) { - for (const { client } of replicas) { - if (client) { - promises.push( - this.#execOnNodeClient(client, fn) - ); - } - } - } - } + if (this.pubSubNode) { + this.pubSubNode.client.destroy(); + this.pubSubNode = undefined; + } - if (this.pubSubNode) { - promises.push(this.#execOnNodeClient(this.pubSubNode.client, fn)); - this.pubSubNode = undefined; - } + this.#resetSlots(); + this.nodeByAddress.clear(); + } - this.#resetSlots(); - this.nodeByAddress.clear(); + *#clients() { + for (const master of this.masters) { + if (master.client) { + yield master.client; + } - await Promise.allSettled(promises); + if (master.pubSub) { + yield master.pubSub.client; + } } - #execOnNodeClient( - client: ClientOrPromise, - fn: (client: RedisClientType) => Promise - ) { - return types.isPromise(client) ? - client.then(fn) : - fn(client); + for (const replica of this.replicas) { + if (replica.client) { + yield replica.client; + } } + } - getClient( - firstKey: RedisCommandArgument | undefined, - isReadonly: boolean | undefined - ): ClientOrPromise { - if (!firstKey) { - return this.nodeClient(this.getRandomNode()); - } + async #destroy(fn: (client: RedisClientType) => Promise): Promise { + this.#isOpen = false; - const slotNumber = calculateSlot(firstKey); - if (!isReadonly) { - return this.nodeClient(this.slots[slotNumber].master); - } + const promises = []; + for (const client of this.#clients()) { + promises.push(fn(client)); + } - return this.nodeClient(this.getSlotRandomNode(slotNumber)); + if (this.pubSubNode) { + promises.push(fn(this.pubSubNode.client)); + this.pubSubNode = undefined; } - *#iterateAllNodes() { - let i = Math.floor(Math.random() * (this.masters.length + this.replicas.length)); - if (i < this.masters.length) { - do { - yield this.masters[i]; - } while (++i < this.masters.length); - - for (const replica of this.replicas) { - yield replica; - } - } else { - i -= this.masters.length; - do { - yield this.replicas[i]; - } while (++i < this.replicas.length); - } + this.#resetSlots(); + this.nodeByAddress.clear(); - while (true) { - for (const master of this.masters) { - yield master; - } + await Promise.allSettled(promises); + } - for (const replica of this.replicas) { - yield replica; - } - } + getClient( + firstKey: RedisArgument | undefined, + isReadonly: boolean | undefined + ) { + if (!firstKey) { + return this.nodeClient(this.getRandomNode()); } - #randomNodeIterator?: IterableIterator>; - - getRandomNode() { - this.#randomNodeIterator ??= this.#iterateAllNodes(); - return this.#randomNodeIterator.next().value as ShardNode; + const slotNumber = calculateSlot(firstKey); + if (!isReadonly) { + return this.nodeClient(this.slots[slotNumber].master); } - *#slotNodesIterator(slot: ShardWithReplicas) { - let i = Math.floor(Math.random() * (1 + slot.replicas.length)); - if (i < slot.replicas.length) { - do { - yield slot.replicas[i]; - } while (++i < slot.replicas.length); - } - - while (true) { - yield slot.master; - - for (const replica of slot.replicas) { - yield replica; - } - } + return this.nodeClient(this.getSlotRandomNode(slotNumber)); + } + + *#iterateAllNodes() { + let i = Math.floor(Math.random() * (this.masters.length + this.replicas.length)); + if (i < this.masters.length) { + do { + yield this.masters[i]; + } while (++i < this.masters.length); + + for (const replica of this.replicas) { + yield replica; + } + } else { + i -= this.masters.length; + do { + yield this.replicas[i]; + } while (++i < this.replicas.length); } - getSlotRandomNode(slotNumber: number) { - const slot = this.slots[slotNumber]; - if (!slot.replicas?.length) { - return slot.master; - } + while (true) { + for (const master of this.masters) { + yield master; + } - slot.nodesIterator ??= this.#slotNodesIterator(slot as ShardWithReplicas); - return slot.nodesIterator.next().value as ShardNode; + for (const replica of this.replicas) { + yield replica; + } } + } - getMasterByAddress(address: string) { - const master = this.nodeByAddress.get(address); - if (!master) return; - - return this.nodeClient(master); - } + _randomNodeIterator?: IterableIterator>; - getPubSubClient() { - return this.pubSubNode ? - this.pubSubNode.client : - this.#initiatePubSubClient(); - } + getRandomNode() { + this._randomNodeIterator ??= this.#iterateAllNodes(); + return this._randomNodeIterator.next().value as ShardNode; + } - async #initiatePubSubClient(toResubscribe?: PubSubToResubscribe) { - const index = Math.floor(Math.random() * (this.masters.length + this.replicas.length)), - node = index < this.masters.length ? - this.masters[index] : - this.replicas[index - this.masters.length]; - - this.pubSubNode = { - address: node.address, - client: this.#createClient(node, true) - .then(async client => { - if (toResubscribe) { - await Promise.all([ - client.extendPubSubListeners(PubSubType.CHANNELS, toResubscribe[PubSubType.CHANNELS]), - client.extendPubSubListeners(PubSubType.PATTERNS, toResubscribe[PubSubType.PATTERNS]) - ]); - } - - this.pubSubNode!.client = client; - return client; - }) - .catch(err => { - this.pubSubNode = undefined; - throw err; - }) - }; - - return this.pubSubNode.client as Promise>; + *#slotNodesIterator(slot: ShardWithReplicas) { + let i = Math.floor(Math.random() * (1 + slot.replicas.length)); + if (i < slot.replicas.length) { + do { + yield slot.replicas[i]; + } while (++i < slot.replicas.length); } - async executeUnsubscribeCommand( - unsubscribe: (client: RedisClientType) => Promise - ): Promise { - const client = await this.getPubSubClient(); - await unsubscribe(client); + while (true) { + yield slot.master; - if (!client.isPubSubActive && client.isOpen) { - await client.disconnect(); - this.pubSubNode = undefined; - } + for (const replica of slot.replicas) { + yield replica; + } } + } - getShardedPubSubClient(channel: string) { - const { master } = this.slots[calculateSlot(channel)]; - return master.pubSubClient ?? this.#initiateShardedPubSubClient(master); + getSlotRandomNode(slotNumber: number) { + const slot = this.slots[slotNumber]; + if (!slot.replicas?.length) { + return slot.master; } - #initiateShardedPubSubClient(master: MasterNode) { - const promise = this.#createClient(master, true) - .then(client => { - client.on('server-sunsubscribe', async (channel, listeners) => { - try { - await this.rediscover(client); - const redirectTo = await this.getShardedPubSubClient(channel); - redirectTo.extendPubSubChannelListeners( - PubSubType.SHARDED, - channel, - listeners - ); - } catch (err) { - this.#emit('sharded-shannel-moved-error', err, channel, listeners); - } - }); - - master.pubSubClient = client; - return client; - }) - .catch(err => { - master.pubSubClient = undefined; - throw err; - }); + slot.nodesIterator ??= this.#slotNodesIterator(slot as ShardWithReplicas); + return slot.nodesIterator.next().value as ShardNode; + } + + getMasterByAddress(address: string) { + const master = this.nodeByAddress.get(address); + if (!master) return; + + return this.nodeClient(master); + } + + getPubSubClient() { + if (!this.pubSubNode) return this.#initiatePubSubClient(); + + return this.pubSubNode.connectPromise ?? this.pubSubNode.client; + } + + async #initiatePubSubClient(toResubscribe?: PubSubToResubscribe) { + const index = Math.floor(Math.random() * (this.masters.length + this.replicas.length)), + node = index < this.masters.length ? + this.masters[index] : + this.replicas[index - this.masters.length], + client = this.#createClient(node, true); + + this.pubSubNode = { + address: node.address, + client, + connectPromise: client.connect() + .then(async client => { + if (toResubscribe) { + await Promise.all([ + client.extendPubSubListeners(PUBSUB_TYPE.CHANNELS, toResubscribe[PUBSUB_TYPE.CHANNELS]), + client.extendPubSubListeners(PUBSUB_TYPE.PATTERNS, toResubscribe[PUBSUB_TYPE.PATTERNS]) + ]); + } + + this.pubSubNode!.connectPromise = undefined; + return client; + }) + .catch(err => { + this.pubSubNode = undefined; + throw err; + }) + }; + + return this.pubSubNode.connectPromise!; + } + + async executeUnsubscribeCommand( + unsubscribe: (client: RedisClientType) => Promise + ): Promise { + const client = await this.getPubSubClient(); + await unsubscribe(client); + + if (!client.isPubSubActive) { + client.destroy(); + this.pubSubNode = undefined; + } + } - master.pubSubClient = promise; + getShardedPubSubClient(channel: string) { + const { master } = this.slots[calculateSlot(channel)]; + if (!master.pubSub) return this.#initiateShardedPubSubClient(master); + return master.pubSub.connectPromise ?? master.pubSub.client; + } - return promise; - } + async #initiateShardedPubSubClient(master: MasterNode) { + const client = this.#createClient(master, true) + .on('server-sunsubscribe', async (channel, listeners) => { + try { + await this.rediscover(client); + const redirectTo = await this.getShardedPubSubClient(channel); + await redirectTo.extendPubSubChannelListeners( + PUBSUB_TYPE.SHARDED, + channel, + listeners + ); + } catch (err) { + this.#emit('sharded-shannel-moved-error', err, channel, listeners); + } + }); + + master.pubSub = { + client, + connectPromise: client.connect() + .then(client => { + master.pubSub!.connectPromise = undefined; + return client; + }) + .catch(err => { + master.pubSub = undefined; + throw err; + }) + }; + + return master.pubSub.connectPromise!; + } + + async executeShardedUnsubscribeCommand( + channel: string, + unsubscribe: (client: RedisClientType) => Promise + ) { + const { master } = this.slots[calculateSlot(channel)]; + if (!master.pubSub) return; - async executeShardedUnsubscribeCommand( - channel: string, - unsubscribe: (client: RedisClientType) => Promise - ): Promise { - const { master } = this.slots[calculateSlot(channel)]; - if (!master.pubSubClient) return Promise.resolve(); + const client = master.pubSub.connectPromise ? + await master.pubSub.connectPromise : + master.pubSub.client; - const client = await master.pubSubClient; - await unsubscribe(client); + await unsubscribe(client); - if (!client.isPubSubActive && client.isOpen) { - await client.disconnect(); - master.pubSubClient = undefined; - } + if (!client.isPubSubActive) { + client.destroy(); + master.pubSub = undefined; } + } } diff --git a/packages/client/lib/cluster/commands.ts b/packages/client/lib/cluster/commands.ts deleted file mode 100644 index 9027c5c0b5e..00000000000 --- a/packages/client/lib/cluster/commands.ts +++ /dev/null @@ -1,670 +0,0 @@ - -import * as APPEND from '../commands/APPEND'; -import * as BITCOUNT from '../commands/BITCOUNT'; -import * as BITFIELD_RO from '../commands/BITFIELD_RO'; -import * as BITFIELD from '../commands/BITFIELD'; -import * as BITOP from '../commands/BITOP'; -import * as BITPOS from '../commands/BITPOS'; -import * as BLMOVE from '../commands/BLMOVE'; -import * as BLMPOP from '../commands/BLMPOP'; -import * as BLPOP from '../commands/BLPOP'; -import * as BRPOP from '../commands/BRPOP'; -import * as BRPOPLPUSH from '../commands/BRPOPLPUSH'; -import * as BZMPOP from '../commands/BZMPOP'; -import * as BZPOPMAX from '../commands/BZPOPMAX'; -import * as BZPOPMIN from '../commands/BZPOPMIN'; -import * as COPY from '../commands/COPY'; -import * as DECR from '../commands/DECR'; -import * as DECRBY from '../commands/DECRBY'; -import * as DEL from '../commands/DEL'; -import * as DUMP from '../commands/DUMP'; -import * as EVAL_RO from '../commands/EVAL_RO'; -import * as EVAL from '../commands/EVAL'; -import * as EVALSHA_RO from '../commands/EVALSHA_RO'; -import * as EVALSHA from '../commands/EVALSHA'; -import * as EXISTS from '../commands/EXISTS'; -import * as EXPIRE from '../commands/EXPIRE'; -import * as EXPIREAT from '../commands/EXPIREAT'; -import * as EXPIRETIME from '../commands/EXPIRETIME'; -import * as FCALL_RO from '../commands/FCALL_RO'; -import * as FCALL from '../commands/FCALL'; -import * as GEOADD from '../commands/GEOADD'; -import * as GEODIST from '../commands/GEODIST'; -import * as GEOHASH from '../commands/GEOHASH'; -import * as GEOPOS from '../commands/GEOPOS'; -import * as GEORADIUS_RO_WITH from '../commands/GEORADIUS_RO_WITH'; -import * as GEORADIUS_RO from '../commands/GEORADIUS_RO'; -import * as GEORADIUS_WITH from '../commands/GEORADIUS_WITH'; -import * as GEORADIUS from '../commands/GEORADIUS'; -import * as GEORADIUSBYMEMBER_RO_WITH from '../commands/GEORADIUSBYMEMBER_RO_WITH'; -import * as GEORADIUSBYMEMBER_RO from '../commands/GEORADIUSBYMEMBER_RO'; -import * as GEORADIUSBYMEMBER_WITH from '../commands/GEORADIUSBYMEMBER_WITH'; -import * as GEORADIUSBYMEMBER from '../commands/GEORADIUSBYMEMBER'; -import * as GEORADIUSBYMEMBERSTORE from '../commands/GEORADIUSBYMEMBERSTORE'; -import * as GEORADIUSSTORE from '../commands/GEORADIUSSTORE'; -import * as GEOSEARCH_WITH from '../commands/GEOSEARCH_WITH'; -import * as GEOSEARCH from '../commands/GEOSEARCH'; -import * as GEOSEARCHSTORE from '../commands/GEOSEARCHSTORE'; -import * as GET from '../commands/GET'; -import * as GETBIT from '../commands/GETBIT'; -import * as GETDEL from '../commands/GETDEL'; -import * as GETEX from '../commands/GETEX'; -import * as GETRANGE from '../commands/GETRANGE'; -import * as GETSET from '../commands/GETSET'; -import * as HDEL from '../commands/HDEL'; -import * as HEXISTS from '../commands/HEXISTS'; -import * as HEXPIRE from '../commands/HEXPIRE'; -import * as HEXPIREAT from '../commands/HEXPIREAT'; -import * as HEXPIRETIME from '../commands/HEXPIRETIME'; -import * as HGET from '../commands/HGET'; -import * as HGETALL from '../commands/HGETALL'; -import * as HINCRBY from '../commands/HINCRBY'; -import * as HINCRBYFLOAT from '../commands/HINCRBYFLOAT'; -import * as HKEYS from '../commands/HKEYS'; -import * as HLEN from '../commands/HLEN'; -import * as HMGET from '../commands/HMGET'; -import * as HPERSIST from '../commands/HPERSIST'; -import * as HPEXPIRE from '../commands/HPEXPIRE'; -import * as HPEXPIREAT from '../commands/HPEXPIREAT'; -import * as HPEXPIRETIME from '../commands/HPEXPIRETIME'; -import * as HPTTL from '../commands/HPTTL'; -import * as HRANDFIELD_COUNT_WITHVALUES from '../commands/HRANDFIELD_COUNT_WITHVALUES'; -import * as HRANDFIELD_COUNT from '../commands/HRANDFIELD_COUNT'; -import * as HRANDFIELD from '../commands/HRANDFIELD'; -import * as HSCAN from '../commands/HSCAN'; -import * as HSCAN_NOVALUES from '../commands/HSCAN_NOVALUES'; -import * as HSET from '../commands/HSET'; -import * as HSETNX from '../commands/HSETNX'; -import * as HSTRLEN from '../commands/HSTRLEN'; -import * as HTTL from '../commands/HTTL'; -import * as HVALS from '../commands/HVALS'; -import * as INCR from '../commands/INCR'; -import * as INCRBY from '../commands/INCRBY'; -import * as INCRBYFLOAT from '../commands/INCRBYFLOAT'; -import * as LCS_IDX_WITHMATCHLEN from '../commands/LCS_IDX_WITHMATCHLEN'; -import * as LCS_IDX from '../commands/LCS_IDX'; -import * as LCS_LEN from '../commands/LCS_LEN'; -import * as LCS from '../commands/LCS'; -import * as LINDEX from '../commands/LINDEX'; -import * as LINSERT from '../commands/LINSERT'; -import * as LLEN from '../commands/LLEN'; -import * as LMOVE from '../commands/LMOVE'; -import * as LMPOP from '../commands/LMPOP'; -import * as LPOP_COUNT from '../commands/LPOP_COUNT'; -import * as LPOP from '../commands/LPOP'; -import * as LPOS_COUNT from '../commands/LPOS_COUNT'; -import * as LPOS from '../commands/LPOS'; -import * as LPUSH from '../commands/LPUSH'; -import * as LPUSHX from '../commands/LPUSHX'; -import * as LRANGE from '../commands/LRANGE'; -import * as LREM from '../commands/LREM'; -import * as LSET from '../commands/LSET'; -import * as LTRIM from '../commands/LTRIM'; -import * as MGET from '../commands/MGET'; -import * as MIGRATE from '../commands/MIGRATE'; -import * as MSET from '../commands/MSET'; -import * as MSETNX from '../commands/MSETNX'; -import * as OBJECT_ENCODING from '../commands/OBJECT_ENCODING'; -import * as OBJECT_FREQ from '../commands/OBJECT_FREQ'; -import * as OBJECT_IDLETIME from '../commands/OBJECT_IDLETIME'; -import * as OBJECT_REFCOUNT from '../commands/OBJECT_REFCOUNT'; -import * as PERSIST from '../commands/PERSIST'; -import * as PEXPIRE from '../commands/PEXPIRE'; -import * as PEXPIREAT from '../commands/PEXPIREAT'; -import * as PEXPIRETIME from '../commands/PEXPIRETIME'; -import * as PFADD from '../commands/PFADD'; -import * as PFCOUNT from '../commands/PFCOUNT'; -import * as PFMERGE from '../commands/PFMERGE'; -import * as PSETEX from '../commands/PSETEX'; -import * as PTTL from '../commands/PTTL'; -import * as PUBLISH from '../commands/PUBLISH'; -import * as RENAME from '../commands/RENAME'; -import * as RENAMENX from '../commands/RENAMENX'; -import * as RESTORE from '../commands/RESTORE'; -import * as RPOP_COUNT from '../commands/RPOP_COUNT'; -import * as RPOP from '../commands/RPOP'; -import * as RPOPLPUSH from '../commands/RPOPLPUSH'; -import * as RPUSH from '../commands/RPUSH'; -import * as RPUSHX from '../commands/RPUSHX'; -import * as SADD from '../commands/SADD'; -import * as SCARD from '../commands/SCARD'; -import * as SDIFF from '../commands/SDIFF'; -import * as SDIFFSTORE from '../commands/SDIFFSTORE'; -import * as SET from '../commands/SET'; -import * as SETBIT from '../commands/SETBIT'; -import * as SETEX from '../commands/SETEX'; -import * as SETNX from '../commands/SETNX'; -import * as SETRANGE from '../commands/SETRANGE'; -import * as SINTER from '../commands/SINTER'; -import * as SINTERCARD from '../commands/SINTERCARD'; -import * as SINTERSTORE from '../commands/SINTERSTORE'; -import * as SISMEMBER from '../commands/SISMEMBER'; -import * as SMEMBERS from '../commands/SMEMBERS'; -import * as SMISMEMBER from '../commands/SMISMEMBER'; -import * as SMOVE from '../commands/SMOVE'; -import * as SORT_RO from '../commands/SORT_RO'; -import * as SORT_STORE from '../commands/SORT_STORE'; -import * as SORT from '../commands/SORT'; -import * as SPOP from '../commands/SPOP'; -import * as SPUBLISH from '../commands/SPUBLISH'; -import * as SRANDMEMBER_COUNT from '../commands/SRANDMEMBER_COUNT'; -import * as SRANDMEMBER from '../commands/SRANDMEMBER'; -import * as SREM from '../commands/SREM'; -import * as SSCAN from '../commands/SSCAN'; -import * as STRLEN from '../commands/STRLEN'; -import * as SUNION from '../commands/SUNION'; -import * as SUNIONSTORE from '../commands/SUNIONSTORE'; -import * as TOUCH from '../commands/TOUCH'; -import * as TTL from '../commands/TTL'; -import * as TYPE from '../commands/TYPE'; -import * as UNLINK from '../commands/UNLINK'; -import * as WATCH from '../commands/WATCH'; -import * as XACK from '../commands/XACK'; -import * as XADD from '../commands/XADD'; -import * as XAUTOCLAIM_JUSTID from '../commands/XAUTOCLAIM_JUSTID'; -import * as XAUTOCLAIM from '../commands/XAUTOCLAIM'; -import * as XCLAIM_JUSTID from '../commands/XCLAIM_JUSTID'; -import * as XCLAIM from '../commands/XCLAIM'; -import * as XDEL from '../commands/XDEL'; -import * as XGROUP_CREATE from '../commands/XGROUP_CREATE'; -import * as XGROUP_CREATECONSUMER from '../commands/XGROUP_CREATECONSUMER'; -import * as XGROUP_DELCONSUMER from '../commands/XGROUP_DELCONSUMER'; -import * as XGROUP_DESTROY from '../commands/XGROUP_DESTROY'; -import * as XGROUP_SETID from '../commands/XGROUP_SETID'; -import * as XINFO_CONSUMERS from '../commands/XINFO_CONSUMERS'; -import * as XINFO_GROUPS from '../commands/XINFO_GROUPS'; -import * as XINFO_STREAM from '../commands/XINFO_STREAM'; -import * as XLEN from '../commands/XLEN'; -import * as XPENDING_RANGE from '../commands/XPENDING_RANGE'; -import * as XPENDING from '../commands/XPENDING'; -import * as XRANGE from '../commands/XRANGE'; -import * as XREAD from '../commands/XREAD'; -import * as XREADGROUP from '../commands/XREADGROUP'; -import * as XREVRANGE from '../commands/XREVRANGE'; -import * as XSETID from '../commands/XSETID'; -import * as XTRIM from '../commands/XTRIM'; -import * as ZADD from '../commands/ZADD'; -import * as ZCARD from '../commands/ZCARD'; -import * as ZCOUNT from '../commands/ZCOUNT'; -import * as ZDIFF_WITHSCORES from '../commands/ZDIFF_WITHSCORES'; -import * as ZDIFF from '../commands/ZDIFF'; -import * as ZDIFFSTORE from '../commands/ZDIFFSTORE'; -import * as ZINCRBY from '../commands/ZINCRBY'; -import * as ZINTER_WITHSCORES from '../commands/ZINTER_WITHSCORES'; -import * as ZINTER from '../commands/ZINTER'; -import * as ZINTERCARD from '../commands/ZINTERCARD'; -import * as ZINTERSTORE from '../commands/ZINTERSTORE'; -import * as ZLEXCOUNT from '../commands/ZLEXCOUNT'; -import * as ZMPOP from '../commands/ZMPOP'; -import * as ZMSCORE from '../commands/ZMSCORE'; -import * as ZPOPMAX_COUNT from '../commands/ZPOPMAX_COUNT'; -import * as ZPOPMAX from '../commands/ZPOPMAX'; -import * as ZPOPMIN_COUNT from '../commands/ZPOPMIN_COUNT'; -import * as ZPOPMIN from '../commands/ZPOPMIN'; -import * as ZRANDMEMBER_COUNT_WITHSCORES from '../commands/ZRANDMEMBER_COUNT_WITHSCORES'; -import * as ZRANDMEMBER_COUNT from '../commands/ZRANDMEMBER_COUNT'; -import * as ZRANDMEMBER from '../commands/ZRANDMEMBER'; -import * as ZRANGE_WITHSCORES from '../commands/ZRANGE_WITHSCORES'; -import * as ZRANGE from '../commands/ZRANGE'; -import * as ZRANGEBYLEX from '../commands/ZRANGEBYLEX'; -import * as ZRANGEBYSCORE_WITHSCORES from '../commands/ZRANGEBYSCORE_WITHSCORES'; -import * as ZRANGEBYSCORE from '../commands/ZRANGEBYSCORE'; -import * as ZRANGESTORE from '../commands/ZRANGESTORE'; -import * as ZRANK from '../commands/ZRANK'; -import * as ZREM from '../commands/ZREM'; -import * as ZREMRANGEBYLEX from '../commands/ZREMRANGEBYLEX'; -import * as ZREMRANGEBYRANK from '../commands/ZREMRANGEBYRANK'; -import * as ZREMRANGEBYSCORE from '../commands/ZREMRANGEBYSCORE'; -import * as ZREVRANK from '../commands/ZREVRANK'; -import * as ZSCAN from '../commands/ZSCAN'; -import * as ZSCORE from '../commands/ZSCORE'; -import * as ZUNION_WITHSCORES from '../commands/ZUNION_WITHSCORES'; -import * as ZUNION from '../commands/ZUNION'; -import * as ZUNIONSTORE from '../commands/ZUNIONSTORE'; - -export default { - APPEND, - append: APPEND, - BITCOUNT, - bitCount: BITCOUNT, - BITFIELD_RO, - bitFieldRo: BITFIELD_RO, - BITFIELD, - bitField: BITFIELD, - BITOP, - bitOp: BITOP, - BITPOS, - bitPos: BITPOS, - BLMOVE, - blMove: BLMOVE, - BLMPOP, - blmPop: BLMPOP, - BLPOP, - blPop: BLPOP, - BRPOP, - brPop: BRPOP, - BRPOPLPUSH, - brPopLPush: BRPOPLPUSH, - BZMPOP, - bzmPop: BZMPOP, - BZPOPMAX, - bzPopMax: BZPOPMAX, - BZPOPMIN, - bzPopMin: BZPOPMIN, - COPY, - copy: COPY, - DECR, - decr: DECR, - DECRBY, - decrBy: DECRBY, - DEL, - del: DEL, - DUMP, - dump: DUMP, - EVAL_RO, - evalRo: EVAL_RO, - EVAL, - eval: EVAL, - EVALSHA, - evalSha: EVALSHA, - EVALSHA_RO, - evalShaRo: EVALSHA_RO, - EXISTS, - exists: EXISTS, - EXPIRE, - expire: EXPIRE, - EXPIREAT, - expireAt: EXPIREAT, - EXPIRETIME, - expireTime: EXPIRETIME, - FCALL_RO, - fCallRo: FCALL_RO, - FCALL, - fCall: FCALL, - GEOADD, - geoAdd: GEOADD, - GEODIST, - geoDist: GEODIST, - GEOHASH, - geoHash: GEOHASH, - GEOPOS, - geoPos: GEOPOS, - GEORADIUS_RO_WITH, - geoRadiusRoWith: GEORADIUS_RO_WITH, - GEORADIUS_RO, - geoRadiusRo: GEORADIUS_RO, - GEORADIUS_WITH, - geoRadiusWith: GEORADIUS_WITH, - GEORADIUS, - geoRadius: GEORADIUS, - GEORADIUSBYMEMBER_RO_WITH, - geoRadiusByMemberRoWith: GEORADIUSBYMEMBER_RO_WITH, - GEORADIUSBYMEMBER_RO, - geoRadiusByMemberRo: GEORADIUSBYMEMBER_RO, - GEORADIUSBYMEMBER_WITH, - geoRadiusByMemberWith: GEORADIUSBYMEMBER_WITH, - GEORADIUSBYMEMBER, - geoRadiusByMember: GEORADIUSBYMEMBER, - GEORADIUSBYMEMBERSTORE, - geoRadiusByMemberStore: GEORADIUSBYMEMBERSTORE, - GEORADIUSSTORE, - geoRadiusStore: GEORADIUSSTORE, - GEOSEARCH_WITH, - geoSearchWith: GEOSEARCH_WITH, - GEOSEARCH, - geoSearch: GEOSEARCH, - GEOSEARCHSTORE, - geoSearchStore: GEOSEARCHSTORE, - GET, - get: GET, - GETBIT, - getBit: GETBIT, - GETDEL, - getDel: GETDEL, - GETEX, - getEx: GETEX, - GETRANGE, - getRange: GETRANGE, - GETSET, - getSet: GETSET, - HDEL, - hDel: HDEL, - HEXISTS, - hExists: HEXISTS, - HEXPIRE, - hExpire: HEXPIRE, - HEXPIREAT, - hExpireAt: HEXPIREAT, - HEXPIRETIME, - hExpireTime: HEXPIRETIME, - HGET, - hGet: HGET, - HGETALL, - hGetAll: HGETALL, - HINCRBY, - hIncrBy: HINCRBY, - HINCRBYFLOAT, - hIncrByFloat: HINCRBYFLOAT, - HKEYS, - hKeys: HKEYS, - HLEN, - hLen: HLEN, - HMGET, - hmGet: HMGET, - HPERSIST, - hPersist: HPERSIST, - HPEXPIRE, - hpExpire: HPEXPIRE, - HPEXPIREAT, - hpExpireAt: HPEXPIREAT, - HPEXPIRETIME, - hpExpireTime: HPEXPIRETIME, - HPTTL, - hpTTL: HPTTL, - HRANDFIELD_COUNT_WITHVALUES, - hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES, - HRANDFIELD_COUNT, - hRandFieldCount: HRANDFIELD_COUNT, - HRANDFIELD, - hRandField: HRANDFIELD, - HSCAN, - hScan: HSCAN, - HSCAN_NOVALUES, - hScanNoValues: HSCAN_NOVALUES, - HSET, - hSet: HSET, - HSETNX, - hSetNX: HSETNX, - HSTRLEN, - hStrLen: HSTRLEN, - HTTL, - hTTL: HTTL, - HVALS, - hVals: HVALS, - INCR, - incr: INCR, - INCRBY, - incrBy: INCRBY, - INCRBYFLOAT, - incrByFloat: INCRBYFLOAT, - LCS_IDX_WITHMATCHLEN, - lcsIdxWithMatchLen: LCS_IDX_WITHMATCHLEN, - LCS_IDX, - lcsIdx: LCS_IDX, - LCS_LEN, - lcsLen: LCS_LEN, - LCS, - lcs: LCS, - LINDEX, - lIndex: LINDEX, - LINSERT, - lInsert: LINSERT, - LLEN, - lLen: LLEN, - LMOVE, - lMove: LMOVE, - LMPOP, - lmPop: LMPOP, - LPOP_COUNT, - lPopCount: LPOP_COUNT, - LPOP, - lPop: LPOP, - LPOS_COUNT, - lPosCount: LPOS_COUNT, - LPOS, - lPos: LPOS, - LPUSH, - lPush: LPUSH, - LPUSHX, - lPushX: LPUSHX, - LRANGE, - lRange: LRANGE, - LREM, - lRem: LREM, - LSET, - lSet: LSET, - LTRIM, - lTrim: LTRIM, - MGET, - mGet: MGET, - MIGRATE, - migrate: MIGRATE, - MSET, - mSet: MSET, - MSETNX, - mSetNX: MSETNX, - OBJECT_ENCODING, - objectEncoding: OBJECT_ENCODING, - OBJECT_FREQ, - objectFreq: OBJECT_FREQ, - OBJECT_IDLETIME, - objectIdleTime: OBJECT_IDLETIME, - OBJECT_REFCOUNT, - objectRefCount: OBJECT_REFCOUNT, - PERSIST, - persist: PERSIST, - PEXPIRE, - pExpire: PEXPIRE, - PEXPIREAT, - pExpireAt: PEXPIREAT, - PEXPIRETIME, - pExpireTime: PEXPIRETIME, - PFADD, - pfAdd: PFADD, - PFCOUNT, - pfCount: PFCOUNT, - PFMERGE, - pfMerge: PFMERGE, - PSETEX, - pSetEx: PSETEX, - PTTL, - pTTL: PTTL, - PUBLISH, - publish: PUBLISH, - RENAME, - rename: RENAME, - RENAMENX, - renameNX: RENAMENX, - RESTORE, - restore: RESTORE, - RPOP_COUNT, - rPopCount: RPOP_COUNT, - RPOP, - rPop: RPOP, - RPOPLPUSH, - rPopLPush: RPOPLPUSH, - RPUSH, - rPush: RPUSH, - RPUSHX, - rPushX: RPUSHX, - SADD, - sAdd: SADD, - SCARD, - sCard: SCARD, - SDIFF, - sDiff: SDIFF, - SDIFFSTORE, - sDiffStore: SDIFFSTORE, - SINTER, - sInter: SINTER, - SINTERCARD, - sInterCard: SINTERCARD, - SINTERSTORE, - sInterStore: SINTERSTORE, - SET, - set: SET, - SETBIT, - setBit: SETBIT, - SETEX, - setEx: SETEX, - SETNX, - setNX: SETNX, - SETRANGE, - setRange: SETRANGE, - SISMEMBER, - sIsMember: SISMEMBER, - SMEMBERS, - sMembers: SMEMBERS, - SMISMEMBER, - smIsMember: SMISMEMBER, - SMOVE, - sMove: SMOVE, - SORT_RO, - sortRo: SORT_RO, - SORT_STORE, - sortStore: SORT_STORE, - SORT, - sort: SORT, - SPOP, - sPop: SPOP, - SPUBLISH, - sPublish: SPUBLISH, - SRANDMEMBER_COUNT, - sRandMemberCount: SRANDMEMBER_COUNT, - SRANDMEMBER, - sRandMember: SRANDMEMBER, - SREM, - sRem: SREM, - SSCAN, - sScan: SSCAN, - STRLEN, - strLen: STRLEN, - SUNION, - sUnion: SUNION, - SUNIONSTORE, - sUnionStore: SUNIONSTORE, - TOUCH, - touch: TOUCH, - TTL, - ttl: TTL, - TYPE, - type: TYPE, - UNLINK, - unlink: UNLINK, - WATCH, - watch: WATCH, - XACK, - xAck: XACK, - XADD, - xAdd: XADD, - XAUTOCLAIM_JUSTID, - xAutoClaimJustId: XAUTOCLAIM_JUSTID, - XAUTOCLAIM, - xAutoClaim: XAUTOCLAIM, - XCLAIM, - xClaim: XCLAIM, - XCLAIM_JUSTID, - xClaimJustId: XCLAIM_JUSTID, - XDEL, - xDel: XDEL, - XGROUP_CREATE, - xGroupCreate: XGROUP_CREATE, - XGROUP_CREATECONSUMER, - xGroupCreateConsumer: XGROUP_CREATECONSUMER, - XGROUP_DELCONSUMER, - xGroupDelConsumer: XGROUP_DELCONSUMER, - XGROUP_DESTROY, - xGroupDestroy: XGROUP_DESTROY, - XGROUP_SETID, - xGroupSetId: XGROUP_SETID, - XINFO_CONSUMERS, - xInfoConsumers: XINFO_CONSUMERS, - XINFO_GROUPS, - xInfoGroups: XINFO_GROUPS, - XINFO_STREAM, - xInfoStream: XINFO_STREAM, - XLEN, - xLen: XLEN, - XPENDING_RANGE, - xPendingRange: XPENDING_RANGE, - XPENDING, - xPending: XPENDING, - XRANGE, - xRange: XRANGE, - XREAD, - xRead: XREAD, - XREADGROUP, - xReadGroup: XREADGROUP, - XREVRANGE, - xRevRange: XREVRANGE, - XSETID, - xSetId: XSETID, - XTRIM, - xTrim: XTRIM, - ZADD, - zAdd: ZADD, - ZCARD, - zCard: ZCARD, - ZCOUNT, - zCount: ZCOUNT, - ZDIFF_WITHSCORES, - zDiffWithScores: ZDIFF_WITHSCORES, - ZDIFF, - zDiff: ZDIFF, - ZDIFFSTORE, - zDiffStore: ZDIFFSTORE, - ZINCRBY, - zIncrBy: ZINCRBY, - ZINTER_WITHSCORES, - zInterWithScores: ZINTER_WITHSCORES, - ZINTER, - zInter: ZINTER, - ZINTERCARD, - zInterCard: ZINTERCARD, - ZINTERSTORE, - zInterStore: ZINTERSTORE, - ZLEXCOUNT, - zLexCount: ZLEXCOUNT, - ZMPOP, - zmPop: ZMPOP, - ZMSCORE, - zmScore: ZMSCORE, - ZPOPMAX_COUNT, - zPopMaxCount: ZPOPMAX_COUNT, - ZPOPMAX, - zPopMax: ZPOPMAX, - ZPOPMIN_COUNT, - zPopMinCount: ZPOPMIN_COUNT, - ZPOPMIN, - zPopMin: ZPOPMIN, - ZRANDMEMBER_COUNT_WITHSCORES, - zRandMemberCountWithScores: ZRANDMEMBER_COUNT_WITHSCORES, - ZRANDMEMBER_COUNT, - zRandMemberCount: ZRANDMEMBER_COUNT, - ZRANDMEMBER, - zRandMember: ZRANDMEMBER, - ZRANGE_WITHSCORES, - zRangeWithScores: ZRANGE_WITHSCORES, - ZRANGE, - zRange: ZRANGE, - ZRANGEBYLEX, - zRangeByLex: ZRANGEBYLEX, - ZRANGEBYSCORE_WITHSCORES, - zRangeByScoreWithScores: ZRANGEBYSCORE_WITHSCORES, - ZRANGEBYSCORE, - zRangeByScore: ZRANGEBYSCORE, - ZRANGESTORE, - zRangeStore: ZRANGESTORE, - ZRANK, - zRank: ZRANK, - ZREM, - zRem: ZREM, - ZREMRANGEBYLEX, - zRemRangeByLex: ZREMRANGEBYLEX, - ZREMRANGEBYRANK, - zRemRangeByRank: ZREMRANGEBYRANK, - ZREMRANGEBYSCORE, - zRemRangeByScore: ZREMRANGEBYSCORE, - ZREVRANK, - zRevRank: ZREVRANK, - ZSCAN, - zScan: ZSCAN, - ZSCORE, - zScore: ZSCORE, - ZUNION_WITHSCORES, - zUnionWithScores: ZUNION_WITHSCORES, - ZUNION, - zUnion: ZUNION, - ZUNIONSTORE, - zUnionStore: ZUNIONSTORE -}; diff --git a/packages/client/lib/cluster/index.spec.ts b/packages/client/lib/cluster/index.spec.ts index 569d716272a..4db5f32e853 100644 --- a/packages/client/lib/cluster/index.spec.ts +++ b/packages/client/lib/cluster/index.spec.ts @@ -1,389 +1,342 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; import RedisCluster from '.'; -import { ClusterSlotStates } from '../commands/CLUSTER_SETSLOT'; -import { commandOptions } from '../command-options'; import { SQUARE_SCRIPT } from '../client/index.spec'; import { RootNodesUnavailableError } from '../errors'; import { spy } from 'sinon'; -import { promiseTimeout } from '../utils'; import RedisClient from '../client'; describe('Cluster', () => { - testUtils.testWithCluster('sendCommand', async cluster => { - assert.equal( - await cluster.sendCommand(undefined, true, ['PING']), - 'PONG' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('sendCommand', async cluster => { + assert.equal( + await cluster.sendCommand(undefined, true, ['PING']), + 'PONG' + ); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('isOpen', async cluster => { + assert.equal(cluster.isOpen, true); + await cluster.destroy(); + assert.equal(cluster.isOpen, false); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('connect should throw if already connected', async cluster => { + await assert.rejects(cluster.connect()); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('multi', async cluster => { + const key = 'key'; + assert.deepEqual( + await cluster.multi() + .set(key, 'value') + .get(key) + .exec(), + ['OK', 'value'] + ); + }, GLOBAL.CLUSTERS.OPEN); + + testUtils.testWithCluster('scripts', async cluster => { + const [, reply] = await Promise.all([ + cluster.set('key', '2'), + cluster.square('key') + ]); + + assert.equal(reply, 4); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + scripts: { + square: SQUARE_SCRIPT + } + } + }); + + it('should throw RootNodesUnavailableError', async () => { + const cluster = RedisCluster.create({ + rootNodes: [] + }); - testUtils.testWithCluster('isOpen', async cluster => { - assert.equal(cluster.isOpen, true); - await cluster.disconnect(); - assert.equal(cluster.isOpen, false); - }, GLOBAL.CLUSTERS.OPEN); + try { + await assert.rejects( + cluster.connect(), + RootNodesUnavailableError + ); + } catch (err) { + await cluster.disconnect(); + throw err; + } + }); + + testUtils.testWithCluster('should handle live resharding', async cluster => { + const slot = 12539, + key = 'key', + value = 'value'; + await cluster.set(key, value); + + const importing = cluster.slots[0].master, + migrating = cluster.slots[slot].master, + [importingClient, migratingClient] = await Promise.all([ + cluster.nodeClient(importing), + cluster.nodeClient(migrating) + ]); + + await Promise.all([ + importingClient.clusterSetSlot(slot, 'IMPORTING', migrating.id), + migratingClient.clusterSetSlot(slot, 'MIGRATING', importing.id) + ]); + + // should be able to get the key from the migrating node + assert.equal( + await cluster.get(key), + value + ); + + await migratingClient.migrate( + importing.host, + importing.port, + key, + 0, + 10 + ); + + // should be able to get the key from the importing node using `ASKING` + assert.equal( + await cluster.get(key), + value + ); + + await Promise.all([ + importingClient.clusterSetSlot(slot, 'NODE', importing.id), + migratingClient.clusterSetSlot(slot, 'NODE', importing.id), + ]); + + // should handle `MOVED` errors + assert.equal( + await cluster.get(key), + value + ); + }, { + serverArguments: [], + numberOfMasters: 2 + }); + + testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => { + const totalNodes = cluster.masters.length + cluster.replicas.length, + ids = new Set(); + for (let i = 0; i < totalNodes; i++) { + ids.add(cluster.getRandomNode().id); + } + + assert.equal(ids.size, totalNodes); + }, GLOBAL.CLUSTERS.WITH_REPLICAS); + + testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => { + const totalNodes = 1 + cluster.slots[0].replicas!.length, + ids = new Set(); + for (let i = 0; i < totalNodes; i++) { + ids.add(cluster.getSlotRandomNode(0).id); + } + + assert.equal(ids.size, totalNodes); + }, GLOBAL.CLUSTERS.WITH_REPLICAS); + + testUtils.testWithCluster('cluster topology', async cluster => { + assert.equal(cluster.slots.length, 16384); + const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS; + assert.equal(cluster.masters.length, numberOfMasters); + assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters); + assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas); + }, GLOBAL.CLUSTERS.WITH_REPLICAS); + + testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => { + const masters = cluster.getMasters(); + assert.ok(Array.isArray(masters)); + for (const master of masters) { + assert.equal(typeof master.id, 'string'); + assert.ok(master.client instanceof RedisClient); + } + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + minimizeConnections: undefined // reset to default + } + }); + + testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => { + const master = cluster.getSlotMaster(0); + assert.equal(typeof master.id, 'string'); + assert.ok(master.client instanceof RedisClient); + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + minimizeConnections: undefined // reset to default + } + }); + + testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => { + await assert.rejects(cluster.mGet(['a', 'b'])); + }, GLOBAL.CLUSTERS.OPEN); + + describe('minimizeConnections', () => { + testUtils.testWithCluster('false', async cluster => { + for (const master of cluster.masters) { + assert.ok(master.client instanceof RedisClient); + } + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + minimizeConnections: false + } + }); - testUtils.testWithCluster('connect should throw if already connected', async cluster => { - await assert.rejects(cluster.connect()); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('true', async cluster => { + for (const master of cluster.masters) { + assert.equal(master.client, undefined); + } + }, { + ...GLOBAL.CLUSTERS.OPEN, + clusterConfiguration: { + minimizeConnections: true + } + }); + }); - testUtils.testWithCluster('multi', async cluster => { - const key = 'key'; - assert.deepEqual( - await cluster.multi() - .set(key, 'value') - .get(key) - .exec(), - ['OK', 'value'] - ); + describe('PubSub', () => { + testUtils.testWithCluster('subscribe & unsubscribe', async cluster => { + const listener = spy(); + + await cluster.subscribe('channel', listener); + + await Promise.all([ + waitTillBeenCalled(listener), + cluster.publish('channel', 'message') + ]); + + assert.ok(listener.calledOnceWithExactly('message', 'channel')); + + await cluster.unsubscribe('channel', listener); + + assert.equal(cluster.pubSubNode, undefined); }, GLOBAL.CLUSTERS.OPEN); - testUtils.testWithCluster('scripts', async cluster => { - assert.equal( - await cluster.square(2), - 4 - ); - }, { - ...GLOBAL.CLUSTERS.OPEN, - clusterConfiguration: { - scripts: { - square: SQUARE_SCRIPT - } - } - }); + testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => { + const listener = spy(); - it('should throw RootNodesUnavailableError', async () => { - const cluster = RedisCluster.create({ - rootNodes: [] - }); - - try { - await assert.rejects( - cluster.connect(), - RootNodesUnavailableError - ); - } catch (err) { - await cluster.disconnect(); - throw err; - } - }); + await cluster.pSubscribe('channe*', listener); - testUtils.testWithCluster('should handle live resharding', async cluster => { - const slot = 12539, - key = 'key', - value = 'value'; - await cluster.set(key, value); - - const importing = cluster.slots[0].master, - migrating = cluster.slots[slot].master, - [ importingClient, migratingClient ] = await Promise.all([ - cluster.nodeClient(importing), - cluster.nodeClient(migrating) - ]); - - await Promise.all([ - importingClient.clusterSetSlot(slot, ClusterSlotStates.IMPORTING, migrating.id), - migratingClient.clusterSetSlot(slot, ClusterSlotStates.MIGRATING, importing.id) - ]); + await Promise.all([ + waitTillBeenCalled(listener), + cluster.publish('channel', 'message') + ]); - // should be able to get the key from the migrating node - assert.equal( - await cluster.get(key), - value - ); + assert.ok(listener.calledOnceWithExactly('message', 'channel')); - await migratingClient.migrate( - importing.host, - importing.port, - key, - 0, - 10 - ); + await cluster.pUnsubscribe('channe*', listener); - // should be able to get the key from the importing node using `ASKING` - assert.equal( - await cluster.get(key), - value - ); + assert.equal(cluster.pubSubNode, undefined); + }, GLOBAL.CLUSTERS.OPEN); - await Promise.all([ - importingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id), - migratingClient.clusterSetSlot(slot, ClusterSlotStates.NODE, importing.id), + testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => { + const listener = spy(); + await cluster.subscribe('channel', listener); + + assert.ok(cluster.pubSubNode); + const [migrating, importing] = cluster.masters[0].address === cluster.pubSubNode.address ? + cluster.masters : + [cluster.masters[1], cluster.masters[0]], + [migratingClient, importingClient] = await Promise.all([ + cluster.nodeClient(migrating), + cluster.nodeClient(importing) ]); - // should handle `MOVED` errors - assert.equal( - await cluster.get(key), - value + const range = cluster.slots[0].master === migrating ? { + key: 'bar', // 5061 + start: 0, + end: 8191 + } : { + key: 'foo', // 12182 + start: 8192, + end: 16383 + }; + + // TODO: is there a better way to migrate slots without causing CLUSTERDOWN? + const promises: Array> = []; + for (let i = range.start; i <= range.end; i++) { + promises.push( + migratingClient.clusterSetSlot(i, 'NODE', importing.id), + importingClient.clusterSetSlot(i, 'NODE', importing.id) ); - }, { - serverArguments: [], - numberOfMasters: 2 - }); + } + await Promise.all(promises); - testUtils.testWithCluster('getRandomNode should spread the the load evenly', async cluster => { - const totalNodes = cluster.masters.length + cluster.replicas.length, - ids = new Set(); - for (let i = 0; i < totalNodes; i++) { - ids.add(cluster.getRandomNode().id); - } - - assert.equal(ids.size, totalNodes); - }, GLOBAL.CLUSTERS.WITH_REPLICAS); - - testUtils.testWithCluster('getSlotRandomNode should spread the the load evenly', async cluster => { - const totalNodes = 1 + cluster.slots[0].replicas!.length, - ids = new Set(); - for (let i = 0; i < totalNodes; i++) { - ids.add(cluster.getSlotRandomNode(0).id); - } - - assert.equal(ids.size, totalNodes); - }, GLOBAL.CLUSTERS.WITH_REPLICAS); - - testUtils.testWithCluster('cluster topology', async cluster => { - assert.equal(cluster.slots.length, 16384); - const { numberOfMasters, numberOfReplicas } = GLOBAL.CLUSTERS.WITH_REPLICAS; - assert.equal(cluster.shards.length, numberOfMasters); - assert.equal(cluster.masters.length, numberOfMasters); - assert.equal(cluster.replicas.length, numberOfReplicas * numberOfMasters); - assert.equal(cluster.nodeByAddress.size, numberOfMasters + numberOfMasters * numberOfReplicas); - }, GLOBAL.CLUSTERS.WITH_REPLICAS); - - testUtils.testWithCluster('getMasters should be backwards competiable (without `minimizeConnections`)', async cluster => { - const masters = cluster.getMasters(); - assert.ok(Array.isArray(masters)); - for (const master of masters) { - assert.equal(typeof master.id, 'string'); - assert.ok(master.client instanceof RedisClient); - } - }, { - ...GLOBAL.CLUSTERS.OPEN, - clusterConfiguration: { - minimizeConnections: undefined // reset to default - } - }); + // make sure to cause `MOVED` error + await cluster.get(range.key); - testUtils.testWithCluster('getSlotMaster should be backwards competiable (without `minimizeConnections`)', async cluster => { - const master = cluster.getSlotMaster(0); - assert.equal(typeof master.id, 'string'); - assert.ok(master.client instanceof RedisClient); + await Promise.all([ + cluster.publish('channel', 'message'), + waitTillBeenCalled(listener) + ]); + + assert.ok(listener.calledOnceWithExactly('message', 'channel')); }, { - ...GLOBAL.CLUSTERS.OPEN, - clusterConfiguration: { - minimizeConnections: undefined // reset to default - } + serverArguments: [], + numberOfMasters: 2, + minimumDockerVersion: [7] }); - testUtils.testWithCluster('should throw CROSSSLOT error', async cluster => { - await assert.rejects(cluster.mGet(['a', 'b'])); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => { + const listener = spy(); - testUtils.testWithCluster('should send commands with commandOptions to correct cluster slot (without redirections)', async cluster => { - // 'a' and 'b' hash to different cluster slots (see previous unit test) - // -> maxCommandRedirections 0: rejects on MOVED/ASK reply - await cluster.set(commandOptions({ isolated: true }), 'a', '1'), - await cluster.set(commandOptions({ isolated: true }), 'b', '2'), + await cluster.sSubscribe('channel', listener); - assert.equal(await cluster.get('a'), '1'); - assert.equal(await cluster.get('b'), '2'); + await Promise.all([ + waitTillBeenCalled(listener), + cluster.sPublish('channel', 'message') + ]); + + assert.ok(listener.calledOnceWithExactly('message', 'channel')); + + await cluster.sUnsubscribe('channel', listener); + + // 10328 is the slot of `channel` + assert.equal(cluster.slots[10328].master.pubSub, undefined); }, { - ...GLOBAL.CLUSTERS.OPEN, - clusterConfiguration: { - maxCommandRedirections: 0 - } + ...GLOBAL.CLUSTERS.OPEN, + minimumDockerVersion: [7] }); - describe('minimizeConnections', () => { - testUtils.testWithCluster('false', async cluster => { - for (const master of cluster.masters) { - assert.ok(master.client instanceof RedisClient); - } - }, { - ...GLOBAL.CLUSTERS.OPEN, - clusterConfiguration: { - minimizeConnections: false - } - }); - - testUtils.testWithCluster('true', async cluster => { - for (const master of cluster.masters) { - assert.equal(master.client, undefined); - } - }, { - ...GLOBAL.CLUSTERS.OPEN, - clusterConfiguration: { - minimizeConnections: true - } - }); - }); + testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => { + const SLOT = 10328, + migrating = cluster.slots[SLOT].master, + importing = cluster.masters.find(master => master !== migrating)!, + [migratingClient, importingClient] = await Promise.all([ + cluster.nodeClient(migrating), + cluster.nodeClient(importing) + ]); - describe('PubSub', () => { - testUtils.testWithCluster('subscribe & unsubscribe', async cluster => { - const listener = spy(); - - await cluster.subscribe('channel', listener); - - await Promise.all([ - waitTillBeenCalled(listener), - cluster.publish('channel', 'message') - ]); - - assert.ok(listener.calledOnceWithExactly('message', 'channel')); - - await cluster.unsubscribe('channel', listener); - - assert.equal(cluster.pubSubNode, undefined); - }, GLOBAL.CLUSTERS.OPEN); - - testUtils.testWithCluster('concurrent UNSUBSCRIBE does not throw an error (#2685)', async cluster => { - const listener = spy(); - await Promise.all([ - cluster.subscribe('1', listener), - cluster.subscribe('2', listener) - ]); - await Promise.all([ - cluster.unsubscribe('1', listener), - cluster.unsubscribe('2', listener) - ]); - }, GLOBAL.CLUSTERS.OPEN); - - testUtils.testWithCluster('psubscribe & punsubscribe', async cluster => { - const listener = spy(); - - await cluster.pSubscribe('channe*', listener); - - await Promise.all([ - waitTillBeenCalled(listener), - cluster.publish('channel', 'message') - ]); - - assert.ok(listener.calledOnceWithExactly('message', 'channel')); - - await cluster.pUnsubscribe('channe*', listener); - - assert.equal(cluster.pubSubNode, undefined); - }, GLOBAL.CLUSTERS.OPEN); - - testUtils.testWithCluster('should move listeners when PubSub node disconnects from the cluster', async cluster => { - const listener = spy(); - await cluster.subscribe('channel', listener); - - assert.ok(cluster.pubSubNode); - const [ migrating, importing ] = cluster.masters[0].address === cluster.pubSubNode.address ? - cluster.masters : - [cluster.masters[1], cluster.masters[0]], - [ migratingClient, importingClient ] = await Promise.all([ - cluster.nodeClient(migrating), - cluster.nodeClient(importing) - ]); - - const range = cluster.slots[0].master === migrating ? { - key: 'bar', // 5061 - start: 0, - end: 8191 - } : { - key: 'foo', // 12182 - start: 8192, - end: 16383 - }; - - await Promise.all([ - migratingClient.clusterDelSlotsRange(range), - importingClient.clusterDelSlotsRange(range), - importingClient.clusterAddSlotsRange(range) - ]); - - // wait for migrating node to be notified about the new topology - while ((await migratingClient.clusterInfo()).state !== 'ok') { - await promiseTimeout(50); - } - - // make sure to cause `MOVED` error - await cluster.get(range.key); - - await Promise.all([ - cluster.publish('channel', 'message'), - waitTillBeenCalled(listener) - ]); - - assert.ok(listener.calledOnceWithExactly('message', 'channel')); - }, { - serverArguments: [], - numberOfMasters: 2, - minimumDockerVersion: [7] - }); - - testUtils.testWithCluster('ssubscribe & sunsubscribe', async cluster => { - const listener = spy(); - - await cluster.sSubscribe('channel', listener); - - await Promise.all([ - waitTillBeenCalled(listener), - cluster.sPublish('channel', 'message') - ]); - - assert.ok(listener.calledOnceWithExactly('message', 'channel')); - - await cluster.sUnsubscribe('channel', listener); - - // 10328 is the slot of `channel` - assert.equal(cluster.slots[10328].master.pubSubClient, undefined); - }, { - ...GLOBAL.CLUSTERS.OPEN, - minimumDockerVersion: [7] - }); - - testUtils.testWithCluster('concurrent SUNSUBCRIBE does not throw an error (#2685)', async cluster => { - const listener = spy(); - await Promise.all([ - await cluster.sSubscribe('1', listener), - await cluster.sSubscribe('2', listener) - ]); - await Promise.all([ - cluster.sUnsubscribe('1', listener), - cluster.sUnsubscribe('2', listener) - ]); - }, { - ...GLOBAL.CLUSTERS.OPEN, - minimumDockerVersion: [7] - }); - - testUtils.testWithCluster('should handle sharded-channel-moved events', async cluster => { - const SLOT = 10328, - migrating = cluster.slots[SLOT].master, - importing = cluster.masters.find(master => master !== migrating)!, - [ migratingClient, importingClient ] = await Promise.all([ - cluster.nodeClient(migrating), - cluster.nodeClient(importing) - ]); - - await Promise.all([ - migratingClient.clusterDelSlots(SLOT), - importingClient.clusterDelSlots(SLOT), - importingClient.clusterAddSlots(SLOT) - ]); - - // wait for migrating node to be notified about the new topology - while ((await migratingClient.clusterInfo()).state !== 'ok') { - await promiseTimeout(50); - } - - const listener = spy(); - - // will trigger `MOVED` error - await cluster.sSubscribe('channel', listener); - - await Promise.all([ - waitTillBeenCalled(listener), - cluster.sPublish('channel', 'message') - ]); - - assert.ok(listener.calledOnceWithExactly('message', 'channel')); - }, { - serverArguments: [], - minimumDockerVersion: [7] - }); + await Promise.all([ + migratingClient.clusterDelSlots(SLOT), + importingClient.clusterDelSlots(SLOT), + importingClient.clusterAddSlots(SLOT), + // cause "topology refresh" on both nodes + migratingClient.clusterSetSlot(SLOT, 'NODE', importing.id), + importingClient.clusterSetSlot(SLOT, 'NODE', importing.id) + ]); + + const listener = spy(); + + // will trigger `MOVED` error + await cluster.sSubscribe('channel', listener); + + await Promise.all([ + waitTillBeenCalled(listener), + cluster.sPublish('channel', 'message') + ]); + + assert.ok(listener.calledOnceWithExactly('message', 'channel')); + }, { + serverArguments: [], + minimumDockerVersion: [7] }); + }); }); diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index 49ac293d6cf..7d01b1a20fe 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -1,424 +1,679 @@ -import COMMANDS from './commands'; -import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisCommandReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, RedisCommandSignature, RedisFunction } from '../commands'; -import { ClientCommandOptions, RedisClientOptions, RedisClientType, WithFunctions, WithModules, WithScripts } from '../client'; +import { RedisClientOptions, RedisClientType } from '../client'; +import { CommandOptions } from '../client/commands-queue'; +import { Command, CommandArguments, CommanderConfig, CommandSignature, /*CommandPolicies, CommandWithPoliciesSignature,*/ TypeMapping, RedisArgument, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions } from '../RESP/types'; +import COMMANDS from '../commands'; +import { EventEmitter } from 'node:events'; +import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; import RedisClusterSlots, { NodeAddressMap, ShardNode } from './cluster-slots'; -import { attachExtensions, transformCommandReply, attachCommands, transformCommandArguments } from '../commander'; -import { EventEmitter } from 'events'; -import RedisClusterMultiCommand, { InstantiableRedisClusterMultiCommandType, RedisClusterMultiCommandType } from './multi-command'; -import { RedisMultiQueuedCommand } from '../multi-command'; +import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi-command'; import { PubSubListener } from '../client/pub-sub'; import { ErrorReply } from '../errors'; +import { RedisTcpSocketOptions } from '../client/socket'; + +interface ClusterCommander< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping, + // POLICIES extends CommandPolicies +> extends CommanderConfig { + commandOptions?: ClusterCommandOptions; +} export type RedisClusterClientOptions = Omit< - RedisClientOptions, - 'modules' | 'functions' | 'scripts' | 'database' + RedisClientOptions, + keyof ClusterCommander >; export interface RedisClusterOptions< - M extends RedisModules = Record, - F extends RedisFunctions = Record, - S extends RedisScripts = Record -> extends RedisExtensions { - /** - * Should contain details for some of the cluster nodes that the client will use to discover - * the "cluster topology". We recommend including details for at least 3 nodes here. - */ - rootNodes: Array; - /** - * Default values used for every client in the cluster. Use this to specify global values, - * for example: ACL credentials, timeouts, TLS configuration etc. - */ - defaults?: Partial; - /** - * When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes. - * Useful for short-term or PubSub-only connections. - */ - minimizeConnections?: boolean; - /** - * When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes. - */ - useReplicas?: boolean; - /** - * The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors. - */ - maxCommandRedirections?: number; - /** - * Mapping between the addresses in the cluster (see `CLUSTER SHARDS`) and the addresses the client should connect to - * Useful when the cluster is running on another network - * - */ - nodeAddressMap?: NodeAddressMap; + M extends RedisModules = RedisModules, + F extends RedisFunctions = RedisFunctions, + S extends RedisScripts = RedisScripts, + RESP extends RespVersions = RespVersions, + TYPE_MAPPING extends TypeMapping = TypeMapping, + // POLICIES extends CommandPolicies = CommandPolicies +> extends ClusterCommander { + /** + * Should contain details for some of the cluster nodes that the client will use to discover + * the "cluster topology". We recommend including details for at least 3 nodes here. + */ + rootNodes: Array; + /** + * Default values used for every client in the cluster. Use this to specify global values, + * for example: ACL credentials, timeouts, TLS configuration etc. + */ + defaults?: Partial; + /** + * When `true`, `.connect()` will only discover the cluster topology, without actually connecting to all the nodes. + * Useful for short-term or PubSub-only connections. + */ + minimizeConnections?: boolean; + /** + * When `true`, distribute load by executing readonly commands (such as `GET`, `GEOSEARCH`, etc.) across all cluster nodes. When `false`, only use master nodes. + */ + // TODO: replicas only mode? + useReplicas?: boolean; + /** + * The maximum number of times a command will be redirected due to `MOVED` or `ASK` errors. + */ + maxCommandRedirections?: number; + /** + * Mapping between the addresses in the cluster (see `CLUSTER SHARDS`) and the addresses the client should connect to + * Useful when the cluster is running on another network + */ + nodeAddressMap?: NodeAddressMap; } -type WithCommands = { - [P in keyof typeof COMMANDS]: RedisCommandSignature<(typeof COMMANDS)[P]>; +// remove once request & response policies are ready +type ClusterCommand< + NAME extends PropertyKey, + COMMAND extends Command +> = COMMAND['FIRST_KEY_INDEX'] extends undefined ? ( + COMMAND['IS_FORWARD_COMMAND'] extends true ? NAME : never +) : NAME; + +// CommandWithPoliciesSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING, POLICIES> +type WithCommands< + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof typeof COMMANDS as ClusterCommand]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>; }; -export type RedisClusterType< - M extends RedisModules = Record, - F extends RedisFunctions = Record, - S extends RedisScripts = Record -> = RedisCluster & WithCommands & WithModules & WithFunctions & WithScripts; - -export default class RedisCluster< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> extends EventEmitter { - static extractFirstKey( - command: RedisCommand, - originalArgs: Array, - redisArgs: RedisCommandArguments - ): RedisCommandArgument | undefined { - if (command.FIRST_KEY_INDEX === undefined) { - return undefined; - } else if (typeof command.FIRST_KEY_INDEX === 'number') { - return redisArgs[command.FIRST_KEY_INDEX]; - } - - return command.FIRST_KEY_INDEX(...originalArgs); - } - - static create< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >(options?: RedisClusterOptions): RedisClusterType { - return new (attachExtensions({ - BaseClass: RedisCluster, - modulesExecutor: RedisCluster.prototype.commandsExecutor, - modules: options?.modules, - functionsExecutor: RedisCluster.prototype.functionsExecutor, - functions: options?.functions, - scriptsExecutor: RedisCluster.prototype.scriptsExecutor, - scripts: options?.scripts - }))(options); - } - - readonly #options: RedisClusterOptions; - - readonly #slots: RedisClusterSlots; - - get slots() { - return this.#slots.slots; - } - - get shards() { - return this.#slots.shards; - } - - get masters() { - return this.#slots.masters; - } - - get replicas() { - return this.#slots.replicas; - } - - get nodeByAddress() { - return this.#slots.nodeByAddress; - } - - get pubSubNode() { - return this.#slots.pubSubNode; - } - - readonly #Multi: InstantiableRedisClusterMultiCommandType; - - get isOpen() { - return this.#slots.isOpen; - } - - constructor(options: RedisClusterOptions) { - super(); - - this.#options = options; - this.#slots = new RedisClusterSlots(options, this.emit.bind(this)); - this.#Multi = RedisClusterMultiCommand.extend(options); - } - - duplicate(overrides?: Partial>): RedisClusterType { - return new (Object.getPrototypeOf(this).constructor)({ - ...this.#options, - ...overrides - }); - } - - connect() { - return this.#slots.connect(); - } - - async commandsExecutor( - command: C, - args: Array - ): Promise> { - const { jsArgs, args: redisArgs, options } = transformCommandArguments(command, args); - return transformCommandReply( - command, - await this.sendCommand( - RedisCluster.extractFirstKey(command, jsArgs, redisArgs), - command.IS_READ_ONLY, - redisArgs, - options - ), - redisArgs.preserve - ); - } +type WithModules< + M extends RedisModules, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof M]: { + [C in keyof M[P] as ClusterCommand]: CommandSignature; + }; +}; - async sendCommand( - firstKey: RedisCommandArgument | undefined, - isReadonly: boolean | undefined, - args: RedisCommandArguments, - options?: ClientCommandOptions - ): Promise { - return this.#execute( - firstKey, - isReadonly, - client => client.sendCommand(args, options) - ); - } +type WithFunctions< + F extends RedisFunctions, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [L in keyof F]: { + [C in keyof F[L] as ClusterCommand]: CommandSignature; + }; +}; - async functionsExecutor( - fn: F, - args: Array, - name: string, - ): Promise> { - const { args: redisArgs, options } = transformCommandArguments(fn, args); - return transformCommandReply( - fn, - await this.executeFunction( - name, - fn, - args, - redisArgs, - options - ), - redisArgs.preserve - ); - } +type WithScripts< + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof S as ClusterCommand]: CommandSignature; +}; - async executeFunction( - name: string, - fn: RedisFunction, - originalArgs: Array, - redisArgs: RedisCommandArguments, - options?: ClientCommandOptions - ): Promise { - return this.#execute( - RedisCluster.extractFirstKey(fn, originalArgs, redisArgs), - fn.IS_READ_ONLY, - client => client.executeFunction(name, fn, redisArgs, options) - ); - } +export type RedisClusterType< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {}, + // POLICIES extends CommandPolicies = {} +> = ( + RedisCluster & + WithCommands & + WithModules & + WithFunctions & + WithScripts +); + +export interface ClusterCommandOptions< + TYPE_MAPPING extends TypeMapping = TypeMapping + // POLICIES extends CommandPolicies = CommandPolicies +> extends CommandOptions { + // policies?: POLICIES; +} - async scriptsExecutor(script: S, args: Array): Promise> { - const { args: redisArgs, options } = transformCommandArguments(script, args); - return transformCommandReply( - script, - await this.executeScript( - script, - args, - redisArgs, - options - ), - redisArgs.preserve - ); - } +type ProxyCluster = RedisCluster; - async executeScript( - script: RedisScript, - originalArgs: Array, - redisArgs: RedisCommandArguments, - options?: ClientCommandOptions - ): Promise { - return this.#execute( - RedisCluster.extractFirstKey(script, originalArgs, redisArgs), - script.IS_READ_ONLY, - client => client.executeScript(script, redisArgs, options) - ); - } +type NamespaceProxyCluster = { _self: ProxyCluster }; - async #execute( - firstKey: RedisCommandArgument | undefined, - isReadonly: boolean | undefined, - executor: (client: RedisClientType) => Promise - ): Promise { - const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16; - let client = await this.#slots.getClient(firstKey, isReadonly); - for (let i = 0;; i++) { - try { - return await executor(client); - } catch (err) { - if (++i > maxCommandRedirections || !(err instanceof ErrorReply)) { - throw err; - } - - if (err.message.startsWith('ASK')) { - const address = err.message.substring(err.message.lastIndexOf(' ') + 1); - let redirectTo = await this.#slots.getMasterByAddress(address); - if (!redirectTo) { - await this.#slots.rediscover(client); - redirectTo = await this.#slots.getMasterByAddress(address); - } - - if (!redirectTo) { - throw new Error(`Cannot find node ${address}`); - } - - await redirectTo.asking(); - client = redirectTo; - continue; - } else if (err.message.startsWith('MOVED')) { - await this.#slots.rediscover(client); - client = await this.#slots.getClient(firstKey, isReadonly); - continue; - } - - throw err; - } +export default class RedisCluster< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping, + // POLICIES extends CommandPolicies +> extends EventEmitter { + static extractFirstKey( + command: C, + args: Parameters, + redisArgs: Array + ) { + let key: RedisArgument | undefined; + switch (typeof command.FIRST_KEY_INDEX) { + case 'number': + key = redisArgs[command.FIRST_KEY_INDEX]; + break; + + case 'function': + key = command.FIRST_KEY_INDEX(...args); + break; + } + + return key; + } + + static #createCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return async function (this: ProxyCluster, ...args: Array) { + const redisArgs = command.transformArguments(...args); + const typeMapping = this._commandOptions?.typeMapping; + + const firstKey = RedisCluster.extractFirstKey( + command, + args, + redisArgs + ); + + const reply = await this.sendCommand( + firstKey, + command.IS_READ_ONLY, + redisArgs, + this._commandOptions, + // command.POLICIES + ); + + return transformReply ? + transformReply(reply, redisArgs.preserve, typeMapping) : + reply; + }; + } + + static #createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return async function (this: NamespaceProxyCluster, ...args: Array) { + const redisArgs = command.transformArguments(...args); + const typeMapping = this._self._commandOptions?.typeMapping; + + const firstKey = RedisCluster.extractFirstKey( + command, + args, + redisArgs + ); + + const reply = await this._self.sendCommand( + firstKey, + command.IS_READ_ONLY, + redisArgs, + this._self._commandOptions, + // command.POLICIES + ); + + return transformReply ? + transformReply(reply, redisArgs.preserve, typeMapping) : + reply; + }; + } + + static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn), + transformReply = getTransformReply(fn, resp); + return async function (this: NamespaceProxyCluster, ...args: Array) { + const fnArgs = fn.transformArguments(...args); + const redisArgs = prefix.concat(fnArgs); + const typeMapping = this._self._commandOptions?.typeMapping; + + const firstKey = RedisCluster.extractFirstKey( + fn, + args, + fnArgs + ); + + const reply = await this._self.sendCommand( + firstKey, + fn.IS_READ_ONLY, + redisArgs, + this._self._commandOptions, + // fn.POLICIES + ); + + return transformReply ? + transformReply(reply, fnArgs.preserve, typeMapping) : + reply; + }; + } + + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const prefix = scriptArgumentsPrefix(script), + transformReply = getTransformReply(script, resp); + return async function (this: ProxyCluster, ...args: Array) { + const scriptArgs = script.transformArguments(...args); + const redisArgs = prefix.concat(scriptArgs); + const typeMapping = this._commandOptions?.typeMapping; + + const firstKey = RedisCluster.extractFirstKey( + script, + args, + scriptArgs + ); + + const reply = await this.executeScript( + script, + firstKey, + script.IS_READ_ONLY, + redisArgs, + this._commandOptions, + // script.POLICIES + ); + + return transformReply ? + transformReply(reply, scriptArgs.preserve, typeMapping) : + reply; + }; + } + + static factory< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {}, + // POLICIES extends CommandPolicies = {} + >(config?: ClusterCommander) { + const Cluster = attachConfig({ + BaseClass: RedisCluster, + commands: COMMANDS, + createCommand: RedisCluster.#createCommand, + createModuleCommand: RedisCluster.#createModuleCommand, + createFunctionCommand: RedisCluster.#createFunctionCommand, + createScriptCommand: RedisCluster.#createScriptCommand, + config + }); + + Cluster.prototype.Multi = RedisClusterMultiCommand.extend(config); + + return (options?: Omit>) => { + // returning a "proxy" to prevent the namespaces._self to leak between "proxies" + return Object.create(new Cluster(options)) as RedisClusterType; + }; + } + + static create< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {}, + // POLICIES extends CommandPolicies = {} + >(options?: RedisClusterOptions) { + return RedisCluster.factory(options)(options); + } + + readonly #options: RedisClusterOptions; + + readonly #slots: RedisClusterSlots; + + private _self = this; + private _commandOptions?: ClusterCommandOptions; + + /** + * An array of the cluster slots, each slot contain its `master` and `replicas`. + * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica). + */ + get slots() { + return this._self.#slots.slots; + } + + /** + * An array of the cluster masters. + * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific master node. + */ + get masters() { + return this._self.#slots.masters; + } + + /** + * An array of the cluster replicas. + * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific replica node. + */ + get replicas() { + return this._self.#slots.replicas; + } + + /** + * A map form a node address (`:`) to its shard, each shard contain its `master` and `replicas`. + * Use with {@link RedisCluster.prototype.nodeClient} to get the client for a specific node (master or replica). + */ + get nodeByAddress() { + return this._self.#slots.nodeByAddress; + } + + /** + * The current pub/sub node. + */ + get pubSubNode() { + return this._self.#slots.pubSubNode; + } + + get isOpen() { + return this._self.#slots.isOpen; + } + + constructor(options: RedisClusterOptions) { + super(); + + this.#options = options; + this.#slots = new RedisClusterSlots(options, this.emit.bind(this)); + + if (options?.commandOptions) { + this._commandOptions = options.commandOptions; + } + } + + duplicate< + _M extends RedisModules = M, + _F extends RedisFunctions = F, + _S extends RedisScripts = S, + _RESP extends RespVersions = RESP, + _TYPE_MAPPING extends TypeMapping = TYPE_MAPPING + >(overrides?: Partial>) { + return new (Object.getPrototypeOf(this).constructor)({ + ...this._self.#options, + commandOptions: this._commandOptions, + ...overrides + }) as RedisClusterType<_M, _F, _S, _RESP, _TYPE_MAPPING>; + } + + async connect() { + await this._self.#slots.connect(); + return this as unknown as RedisClusterType; + } + + withCommandOptions< + OPTIONS extends ClusterCommandOptions, + TYPE_MAPPING extends TypeMapping, + // POLICIES extends CommandPolicies + >(options: OPTIONS) { + const proxy = Object.create(this); + proxy._commandOptions = options; + return proxy as RedisClusterType< + M, + F, + S, + RESP, + TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {} + // POLICIES extends CommandPolicies ? POLICIES : {} + >; + } + + private _commandOptionsProxy< + K extends keyof ClusterCommandOptions, + V extends ClusterCommandOptions[K] + >( + key: K, + value: V + ) { + const proxy = Object.create(this); + proxy._commandOptions = Object.create(this._commandOptions ?? null); + proxy._commandOptions[key] = value; + return proxy as RedisClusterType< + M, + F, + S, + RESP, + K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING + // K extends 'policies' ? V extends CommandPolicies ? V : {} : POLICIES + >; + } + + /** + * Override the `typeMapping` command option + */ + withTypeMapping(typeMapping: TYPE_MAPPING) { + return this._commandOptionsProxy('typeMapping', typeMapping); + } + + // /** + // * Override the `policies` command option + // * TODO + // */ + // withPolicies (policies: POLICIES) { + // return this._commandOptionsProxy('policies', policies); + // } + + async #execute( + firstKey: RedisArgument | undefined, + isReadonly: boolean | undefined, + fn: (client: RedisClientType) => Promise + ): Promise { + const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16; + let client = await this.#slots.getClient(firstKey, isReadonly), + i = 0; + while (true) { + try { + return await fn(client); + } catch (err) { + // TODO: error class + if (++i > maxCommandRedirections || !(err instanceof Error)) { + throw err; } - } - - MULTI(routing?: RedisCommandArgument): RedisClusterMultiCommandType { - return new this.#Multi( - (commands: Array, firstKey?: RedisCommandArgument, chainId?: symbol) => { - return this.#execute( - firstKey, - false, - client => client.multiExecutor(commands, undefined, chainId) - ); - }, - routing - ); - } - - multi = this.MULTI; - async SUBSCRIBE( - channels: string | Array, - listener: PubSubListener, - bufferMode?: T - ) { - return (await this.#slots.getPubSubClient()) - .SUBSCRIBE(channels, listener, bufferMode); - } - - subscribe = this.SUBSCRIBE; - - async UNSUBSCRIBE( - channels?: string | Array, - listener?: PubSubListener, - bufferMode?: T - ) { - return this.#slots.executeUnsubscribeCommand(client => - client.UNSUBSCRIBE(channels, listener, bufferMode) - ); - } - - unsubscribe = this.UNSUBSCRIBE; - - async PSUBSCRIBE( - patterns: string | Array, - listener: PubSubListener, - bufferMode?: T - ) { - return (await this.#slots.getPubSubClient()) - .PSUBSCRIBE(patterns, listener, bufferMode); - } - - pSubscribe = this.PSUBSCRIBE; - - async PUNSUBSCRIBE( - patterns?: string | Array, - listener?: PubSubListener, - bufferMode?: T - ) { - return this.#slots.executeUnsubscribeCommand(client => - client.PUNSUBSCRIBE(patterns, listener, bufferMode) - ); - } - - pUnsubscribe = this.PUNSUBSCRIBE; - - async SSUBSCRIBE( - channels: string | Array, - listener: PubSubListener, - bufferMode?: T - ) { - const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16, - firstChannel = Array.isArray(channels) ? channels[0] : channels; - let client = await this.#slots.getShardedPubSubClient(firstChannel); - for (let i = 0;; i++) { - try { - return await client.SSUBSCRIBE(channels, listener, bufferMode); - } catch (err) { - if (++i > maxCommandRedirections || !(err instanceof ErrorReply)) { - throw err; - } - - if (err.message.startsWith('MOVED')) { - await this.#slots.rediscover(client); - client = await this.#slots.getShardedPubSubClient(firstChannel); - continue; - } - - throw err; - } + if (err.message.startsWith('ASK')) { + const address = err.message.substring(err.message.lastIndexOf(' ') + 1); + let redirectTo = await this.#slots.getMasterByAddress(address); + if (!redirectTo) { + await this.#slots.rediscover(client); + redirectTo = await this.#slots.getMasterByAddress(address); + } + + if (!redirectTo) { + throw new Error(`Cannot find node ${address}`); + } + + await redirectTo.asking(); + client = redirectTo; + continue; + } + + if (err.message.startsWith('MOVED')) { + await this.#slots.rediscover(client); + client = await this.#slots.getClient(firstKey, isReadonly); + continue; } - } - - sSubscribe = this.SSUBSCRIBE; - - SUNSUBSCRIBE( - channels: string | Array, - listener?: PubSubListener, - bufferMode?: T - ) { - return this.#slots.executeShardedUnsubscribeCommand( - Array.isArray(channels) ? channels[0] : channels, - client => client.SUNSUBSCRIBE(channels, listener, bufferMode) - ); - } - - sUnsubscribe = this.SUNSUBSCRIBE; - - quit(): Promise { - return this.#slots.quit(); - } - - disconnect(): Promise { - return this.#slots.disconnect(); - } - - nodeClient(node: ShardNode) { - return this.#slots.nodeClient(node); - } - - getRandomNode() { - return this.#slots.getRandomNode(); - } - getSlotRandomNode(slot: number) { - return this.#slots.getSlotRandomNode(slot); - } + throw err; + } + } + } + + async sendCommand( + firstKey: RedisArgument | undefined, + isReadonly: boolean | undefined, + args: CommandArguments, + options?: ClusterCommandOptions, + // defaultPolicies?: CommandPolicies + ): Promise { + return this._self.#execute( + firstKey, + isReadonly, + client => client.sendCommand(args, options) + ); + } + + executeScript( + script: RedisScript, + firstKey: RedisArgument | undefined, + isReadonly: boolean | undefined, + args: Array, + options?: CommandOptions + ) { + return this._self.#execute( + firstKey, + isReadonly, + client => client.executeScript(script, args, options) + ); + } + + MULTI(routing?: RedisArgument) { + type Multi = new (...args: ConstructorParameters) => RedisClusterMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>; + return new ((this as any).Multi as Multi)( + async (firstKey, isReadonly, commands) => { + const client = await this._self.#slots.getClient(firstKey, isReadonly); + return client._executeMulti(commands); + }, + async (firstKey, isReadonly, commands) => { + const client = await this._self.#slots.getClient(firstKey, isReadonly); + return client._executePipeline(commands); + }, + routing, + this._commandOptions?.typeMapping + ); + } + + multi = this.MULTI; + + async SUBSCRIBE( + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + return (await this._self.#slots.getPubSubClient()) + .SUBSCRIBE(channels, listener, bufferMode); + } + + subscribe = this.SUBSCRIBE; + + async UNSUBSCRIBE( + channels?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ) { + return this._self.#slots.executeUnsubscribeCommand(client => + client.UNSUBSCRIBE(channels, listener, bufferMode) + ); + } + + unsubscribe = this.UNSUBSCRIBE; + + async PSUBSCRIBE( + patterns: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + return (await this._self.#slots.getPubSubClient()) + .PSUBSCRIBE(patterns, listener, bufferMode); + } + + pSubscribe = this.PSUBSCRIBE; + + async PUNSUBSCRIBE( + patterns?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ) { + return this._self.#slots.executeUnsubscribeCommand(client => + client.PUNSUBSCRIBE(patterns, listener, bufferMode) + ); + } + + pUnsubscribe = this.PUNSUBSCRIBE; + + async SSUBSCRIBE( + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + const maxCommandRedirections = this._self.#options.maxCommandRedirections ?? 16, + firstChannel = Array.isArray(channels) ? channels[0] : channels; + let client = await this._self.#slots.getShardedPubSubClient(firstChannel); + for (let i = 0; ; i++) { + try { + return await client.SSUBSCRIBE(channels, listener, bufferMode); + } catch (err) { + if (++i > maxCommandRedirections || !(err instanceof ErrorReply)) { + throw err; + } - /** - * @deprecated use `.masters` instead - */ - getMasters() { - return this.masters; - } + if (err.message.startsWith('MOVED')) { + await this._self.#slots.rediscover(client); + client = await this._self.#slots.getShardedPubSubClient(firstChannel); + continue; + } - /** - * @deprecated use `.slots[]` instead - */ - getSlotMaster(slot: number) { - return this.slots[slot].master; - } + throw err; + } + } + } + + sSubscribe = this.SSUBSCRIBE; + + SUNSUBSCRIBE( + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + return this._self.#slots.executeShardedUnsubscribeCommand( + Array.isArray(channels) ? channels[0] : channels, + client => client.SUNSUBSCRIBE(channels, listener, bufferMode) + ); + } + + sUnsubscribe = this.SUNSUBSCRIBE; + + /** + * @deprecated Use `close` instead. + */ + quit() { + return this._self.#slots.quit(); + } + + /** + * @deprecated Use `destroy` instead. + */ + disconnect() { + return this._self.#slots.disconnect(); + } + + close() { + return this._self.#slots.close(); + } + + destroy() { + return this._self.#slots.destroy(); + } + + nodeClient(node: ShardNode) { + return this._self.#slots.nodeClient(node); + } + + /** + * Returns a random node from the cluster. + * Userful for running "forward" commands (like PUBLISH) on a random node. + */ + getRandomNode() { + return this._self.#slots.getRandomNode(); + } + + /** + * Get a random node from a slot. + * Useful for running readonly commands on a slot. + */ + getSlotRandomNode(slot: number) { + return this._self.#slots.getSlotRandomNode(slot); + } + + /** + * @deprecated use `.masters` instead + * TODO + */ + getMasters() { + return this.masters; + } + + /** + * @deprecated use `.slots[]` instead + * TODO + */ + getSlotMaster(slot: number) { + return this.slots[slot].master; + } } - -attachCommands({ - BaseClass: RedisCluster, - commands: COMMANDS, - executor: RedisCluster.prototype.commandsExecutor -}); diff --git a/packages/client/lib/cluster/multi-command.ts b/packages/client/lib/cluster/multi-command.ts index ef3c7590ec7..2b02e8d7df2 100644 --- a/packages/client/lib/cluster/multi-command.ts +++ b/packages/client/lib/cluster/multi-command.ts @@ -1,141 +1,262 @@ -import COMMANDS from './commands'; -import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandRawReply, RedisFunctions, RedisModules, RedisExtensions, RedisScript, RedisScripts, ExcludeMappedString, RedisFunction } from '../commands'; -import RedisMultiCommand, { RedisMultiQueuedCommand } from '../multi-command'; -import { attachCommands, attachExtensions } from '../commander'; +import COMMANDS from '../commands'; +import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType, RedisMultiQueuedCommand } from '../multi-command'; +import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping, RedisArgument } from '../RESP/types'; +import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander'; import RedisCluster from '.'; -type RedisClusterMultiCommandSignature< - C extends RedisCommand, - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = (...args: Parameters) => RedisClusterMultiCommandType; +type CommandSignature< + REPLIES extends Array, + C extends Command, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = (...args: Parameters) => RedisClusterMultiCommandType< + [...REPLIES, ReplyWithTypeMapping, TYPE_MAPPING>], + M, + F, + S, + RESP, + TYPE_MAPPING +>; type WithCommands< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > = { - [P in keyof typeof COMMANDS]: RedisClusterMultiCommandSignature<(typeof COMMANDS)[P], M, F, S>; + [P in keyof typeof COMMANDS]: CommandSignature; }; type WithModules< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > = { - [P in keyof M as ExcludeMappedString

]: { - [C in keyof M[P] as ExcludeMappedString]: RedisClusterMultiCommandSignature; - }; + [P in keyof M]: { + [C in keyof M[P]]: CommandSignature; + }; }; type WithFunctions< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > = { - [P in keyof F as ExcludeMappedString

]: { - [FF in keyof F[P] as ExcludeMappedString]: RedisClusterMultiCommandSignature; - }; + [L in keyof F]: { + [C in keyof F[L]]: CommandSignature; + }; }; type WithScripts< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > = { - [P in keyof S as ExcludeMappedString

]: RedisClusterMultiCommandSignature; + [P in keyof S]: CommandSignature; }; export type RedisClusterMultiCommandType< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = RedisClusterMultiCommand & WithCommands & WithModules & WithFunctions & WithScripts; - -export type InstantiableRedisClusterMultiCommandType< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts -> = new (...args: ConstructorParameters) => RedisClusterMultiCommandType; - -export type RedisClusterMultiExecutor = (queue: Array, firstKey?: RedisCommandArgument, chainId?: symbol) => Promise>; - -export default class RedisClusterMultiCommand { - readonly #multi = new RedisMultiCommand(); - readonly #executor: RedisClusterMultiExecutor; - #firstKey: RedisCommandArgument | undefined; - - static extend< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >(extensions?: RedisExtensions): InstantiableRedisClusterMultiCommandType { - return attachExtensions({ - BaseClass: RedisClusterMultiCommand, - modulesExecutor: RedisClusterMultiCommand.prototype.commandsExecutor, - modules: extensions?.modules, - functionsExecutor: RedisClusterMultiCommand.prototype.functionsExecutor, - functions: extensions?.functions, - scriptsExecutor: RedisClusterMultiCommand.prototype.scriptsExecutor, - scripts: extensions?.scripts - }); - } - - constructor(executor: RedisClusterMultiExecutor, firstKey?: RedisCommandArgument) { - this.#executor = executor; - this.#firstKey = firstKey; - } - - commandsExecutor(command: RedisCommand, args: Array): this { - const transformedArguments = command.transformArguments(...args); - this.#firstKey ??= RedisCluster.extractFirstKey(command, args, transformedArguments); - return this.addCommand(undefined, transformedArguments, command.transformReply); - } - - addCommand( - firstKey: RedisCommandArgument | undefined, - args: RedisCommandArguments, - transformReply?: RedisCommand['transformReply'] - ): this { - this.#firstKey ??= firstKey; - this.#multi.addCommand(args, transformReply); - return this; - } - - functionsExecutor(fn: RedisFunction, args: Array, name: string): this { - const transformedArguments = this.#multi.addFunction(name, fn, args); - this.#firstKey ??= RedisCluster.extractFirstKey(fn, args, transformedArguments); - return this; - } - - scriptsExecutor(script: RedisScript, args: Array): this { - const transformedArguments = this.#multi.addScript(script, args); - this.#firstKey ??= RedisCluster.extractFirstKey(script, args, transformedArguments); - return this; - } - - async exec(execAsPipeline = false): Promise> { - if (execAsPipeline) { - return this.execAsPipeline(); - } - - return this.#multi.handleExecReplies( - await this.#executor(this.#multi.queue, this.#firstKey, RedisMultiCommand.generateChainId()) - ); - } + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = ( + RedisClusterMultiCommand & + WithCommands & + WithModules & + WithFunctions & + WithScripts +); - EXEC = this.exec; +export type ClusterMultiExecute = ( + firstKey: RedisArgument | undefined, + isReadonly: boolean | undefined, + commands: Array +) => Promise>; - async execAsPipeline(): Promise> { - return this.#multi.transformReplies( - await this.#executor(this.#multi.queue, this.#firstKey) +export default class RedisClusterMultiCommand { + static #createCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return function (this: RedisClusterMultiCommand, ...args: Array) { + const redisArgs = command.transformArguments(...args); + const firstKey = RedisCluster.extractFirstKey( + command, + args, + redisArgs + ); + return this.addCommand( + firstKey, + command.IS_READ_ONLY, + redisArgs, + transformReply + ); + }; + } + + static #createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return function (this: { _self: RedisClusterMultiCommand }, ...args: Array) { + const redisArgs = command.transformArguments(...args), + firstKey = RedisCluster.extractFirstKey( + command, + args, + redisArgs ); - } -} + return this._self.addCommand( + firstKey, + command.IS_READ_ONLY, + redisArgs, + transformReply + ); + }; + } + + static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn), + transformReply = getTransformReply(fn, resp); + return function (this: { _self: RedisClusterMultiCommand }, ...args: Array) { + const fnArgs = fn.transformArguments(...args); + const redisArgs: CommandArguments = prefix.concat(fnArgs); + const firstKey = RedisCluster.extractFirstKey( + fn, + args, + fnArgs + ); + redisArgs.preserve = fnArgs.preserve; + return this._self.addCommand( + firstKey, + fn.IS_READ_ONLY, + redisArgs, + transformReply + ); + }; + } + + static #createScriptCommand(script: RedisScript, resp: RespVersions) { + const transformReply = getTransformReply(script, resp); + return function (this: RedisClusterMultiCommand, ...args: Array) { + const scriptArgs = script.transformArguments(...args); + this.#setState( + RedisCluster.extractFirstKey( + script, + args, + scriptArgs + ), + script.IS_READ_ONLY + ); + this.#multi.addScript( + script, + scriptArgs, + transformReply + ); + return this; + }; + } + + static extend< + M extends RedisModules = Record, + F extends RedisFunctions = Record, + S extends RedisScripts = Record, + RESP extends RespVersions = 2 + >(config?: CommanderConfig) { + return attachConfig({ + BaseClass: RedisClusterMultiCommand, + commands: COMMANDS, + createCommand: RedisClusterMultiCommand.#createCommand, + createModuleCommand: RedisClusterMultiCommand.#createModuleCommand, + createFunctionCommand: RedisClusterMultiCommand.#createFunctionCommand, + createScriptCommand: RedisClusterMultiCommand.#createScriptCommand, + config + }); + } -attachCommands({ - BaseClass: RedisClusterMultiCommand, - commands: COMMANDS, - executor: RedisClusterMultiCommand.prototype.commandsExecutor -}); + readonly #multi = new RedisMultiCommand(); + readonly #executeMulti: ClusterMultiExecute; + readonly #executePipeline: ClusterMultiExecute; + #firstKey: RedisArgument | undefined; + #isReadonly: boolean | undefined = true; + readonly #typeMapping?: TypeMapping; + + constructor( + executeMulti: ClusterMultiExecute, + executePipeline: ClusterMultiExecute, + routing: RedisArgument | undefined, + typeMapping?: TypeMapping + ) { + this.#executeMulti = executeMulti; + this.#executePipeline = executePipeline; + this.#firstKey = routing; + this.#typeMapping = typeMapping; + } + + #setState( + firstKey: RedisArgument | undefined, + isReadonly: boolean | undefined, + ) { + this.#firstKey ??= firstKey; + this.#isReadonly &&= isReadonly; + } + + addCommand( + firstKey: RedisArgument | undefined, + isReadonly: boolean | undefined, + args: CommandArguments, + transformReply?: TransformReply + ) { + this.#setState(firstKey, isReadonly); + this.#multi.addCommand(args, transformReply); + return this; + } + + async exec(execAsPipeline = false) { + if (execAsPipeline) return this.execAsPipeline(); + + return this.#multi.transformReplies( + await this.#executeMulti( + this.#firstKey, + this.#isReadonly, + this.#multi.queue + ), + this.#typeMapping + ) as MultiReplyType; + } + + EXEC = this.exec; + + execTyped(execAsPipeline = false) { + return this.exec(execAsPipeline); + } + + async execAsPipeline() { + if (this.#multi.queue.length === 0) return [] as MultiReplyType; + + return this.#multi.transformReplies( + await this.#executePipeline( + this.#firstKey, + this.#isReadonly, + this.#multi.queue + ), + this.#typeMapping + ) as MultiReplyType; + } + + execAsPipelineTyped() { + return this.execAsPipeline(); + } +} diff --git a/packages/client/lib/command-options.ts b/packages/client/lib/command-options.ts deleted file mode 100644 index 8f91130b557..00000000000 --- a/packages/client/lib/command-options.ts +++ /dev/null @@ -1,14 +0,0 @@ -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?.[symbol] === true; -} diff --git a/packages/client/lib/commander.ts b/packages/client/lib/commander.ts index c04f41e1eb0..4434317d267 100644 --- a/packages/client/lib/commander.ts +++ b/packages/client/lib/commander.ts @@ -1,165 +1,124 @@ - -import { ClientCommandOptions } from './client'; -import { CommandOptions, isCommandOptions } from './command-options'; -import { RedisCommand, RedisCommandArgument, RedisCommandArguments, RedisCommandReply, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts } from './commands'; - -type Instantiable = new (...args: Array) => T; - -type CommandsExecutor = - (command: C, args: Array, name: string) => unknown; - -interface AttachCommandsConfig { - BaseClass: Instantiable; - commands: Record; - executor: CommandsExecutor; +import { Command, CommanderConfig, RedisCommands, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions } from './RESP/types'; + +interface AttachConfigOptions< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions +> { + BaseClass: new (...args: any) => any; + commands: RedisCommands; + createCommand(command: Command, resp: RespVersions): (...args: any) => any; + createModuleCommand(command: Command, resp: RespVersions): (...args: any) => any; + createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions): (...args: any) => any; + createScriptCommand(script: RedisScript, resp: RespVersions): (...args: any) => any; + config?: CommanderConfig; } -export function attachCommands({ - BaseClass, - commands, - executor -}: AttachCommandsConfig): void { - for (const [name, command] of Object.entries(commands)) { - BaseClass.prototype[name] = function (...args: Array): unknown { - return executor.call(this, command, args, name); - }; - } +/* FIXME: better error message / link */ +function throwResp3SearchModuleUnstableError() { + throw new Error('Some RESP3 results for Redis Query Engine responses may change. Refer to the readme for guidance'); } -interface AttachExtensionsConfig { - BaseClass: T; - modulesExecutor: CommandsExecutor; - modules?: RedisModules; - functionsExecutor: CommandsExecutor; - functions?: RedisFunctions; - scriptsExecutor: CommandsExecutor; - scripts?: RedisScripts; -} - -export function attachExtensions(config: AttachExtensionsConfig): any { - let Commander; +export function attachConfig< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions +>({ + BaseClass, + commands, + createCommand, + createModuleCommand, + createFunctionCommand, + createScriptCommand, + config +}: AttachConfigOptions) { + const RESP = config?.RESP ?? 2, + Class: any = class extends BaseClass {}; + + for (const [name, command] of Object.entries(commands)) { + Class.prototype[name] = createCommand(command, RESP); + } + + if (config?.modules) { + for (const [moduleName, module] of Object.entries(config.modules)) { + const fns = Object.create(null); + for (const [name, command] of Object.entries(module)) { + if (config.RESP == 3 && command.unstableResp3 && !config.unstableResp3) { + fns[name] = throwResp3SearchModuleUnstableError; + } else { + fns[name] = createModuleCommand(command, RESP); + } + } - if (config.modules) { - Commander = attachWithNamespaces({ - BaseClass: config.BaseClass, - namespaces: config.modules, - executor: config.modulesExecutor - }); + attachNamespace(Class.prototype, moduleName, fns); } + } - if (config.functions) { - Commander = attachWithNamespaces({ - BaseClass: Commander ?? config.BaseClass, - namespaces: config.functions, - executor: config.functionsExecutor - }); - } + if (config?.functions) { + for (const [library, commands] of Object.entries(config.functions)) { + const fns = Object.create(null); + for (const [name, command] of Object.entries(commands)) { + fns[name] = createFunctionCommand(name, command, RESP); + } - if (config.scripts) { - Commander ??= class extends config.BaseClass {}; - attachCommands({ - BaseClass: Commander, - commands: config.scripts, - executor: config.scriptsExecutor - }); + attachNamespace(Class.prototype, library, fns); } + } - return Commander ?? config.BaseClass; -} + if (config?.scripts) { + for (const [name, script] of Object.entries(config.scripts)) { + Class.prototype[name] = createScriptCommand(script, RESP); + } + } -interface AttachWithNamespacesConfig { - BaseClass: Instantiable; - namespaces: Record>; - executor: CommandsExecutor; + return Class; } -function attachWithNamespaces({ - BaseClass, - namespaces, - executor -}: AttachWithNamespacesConfig): any { - const Commander = class extends BaseClass { - constructor(...args: Array) { - super(...args); - - for (const namespace of Object.keys(namespaces)) { - this[namespace] = Object.create(this[namespace], { - self: { - value: this - } - }); - } - } - }; - - for (const [namespace, commands] of Object.entries(namespaces)) { - Commander.prototype[namespace] = {}; - for (const [name, command] of Object.entries(commands)) { - Commander.prototype[namespace][name] = function (...args: Array): unknown { - return executor.call(this.self, command, args, name); - }; - } +function attachNamespace(prototype: any, name: PropertyKey, fns: any) { + Object.defineProperty(prototype, name, { + get() { + const value = Object.create(fns); + value._self = this; + Object.defineProperty(this, name, { value }); + return value; } - - return Commander; + }); } -export function transformCommandArguments( - command: RedisCommand, - args: Array -): { - jsArgs: Array; - args: RedisCommandArguments; - options: CommandOptions | undefined; -} { - let options; - if (isCommandOptions(args[0])) { - options = args[0]; - args = args.slice(1); - } +export function getTransformReply(command: Command, resp: RespVersions) { + switch (typeof command.transformReply) { + case 'function': + return command.transformReply; - return { - jsArgs: args, - args: command.transformArguments(...args), - options - }; + case 'object': + return command.transformReply[resp]; + } } -export function transformLegacyCommandArguments(args: Array): Array { - return args.flat().map(arg => { - return typeof arg === 'number' || arg instanceof Date ? - arg.toString() : - arg; - }); -} +export function functionArgumentsPrefix(name: string, fn: RedisFunction) { + const prefix: Array = [ + fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL', + name + ]; -export function transformCommandReply( - command: C, - rawReply: unknown, - preserved: unknown -): RedisCommandReply { - if (!command.transformReply) { - return rawReply as RedisCommandReply; - } + if (fn.NUMBER_OF_KEYS !== undefined) { + prefix.push(fn.NUMBER_OF_KEYS.toString()); + } - return command.transformReply(rawReply, preserved); + return prefix; } -export function fCallArguments( - name: RedisCommandArgument, - fn: RedisFunction, - args: RedisCommandArguments -): RedisCommandArguments { - const actualArgs: RedisCommandArguments = [ - fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL', - name - ]; - - if (fn.NUMBER_OF_KEYS !== undefined) { - actualArgs.push(fn.NUMBER_OF_KEYS.toString()); - } +export function scriptArgumentsPrefix(script: RedisScript) { + const prefix: Array = [ + script.IS_READ_ONLY ? 'EVALSHA_RO' : 'EVALSHA', + script.SHA1 + ]; - actualArgs.push(...args); + if (script.NUMBER_OF_KEYS !== undefined) { + prefix.push(script.NUMBER_OF_KEYS.toString()); + } - return actualArgs; + return prefix; } diff --git a/packages/client/lib/commands/ACL_CAT.spec.ts b/packages/client/lib/commands/ACL_CAT.spec.ts index 521871a1c6b..2ce9d7db922 100644 --- a/packages/client/lib/commands/ACL_CAT.spec.ts +++ b/packages/client/lib/commands/ACL_CAT.spec.ts @@ -1,23 +1,31 @@ -import { strict as assert } from 'assert'; -import testUtils from '../test-utils'; -import { transformArguments } from './ACL_CAT'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ACL_CAT from './ACL_CAT'; describe('ACL CAT', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'CAT'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ACL_CAT.transformArguments(), + ['ACL', 'CAT'] + ); + }); - it('with categoryName', () => { - assert.deepEqual( - transformArguments('dangerous'), - ['ACL', 'CAT', 'dangerous'] - ); - }); + it('with categoryName', () => { + assert.deepEqual( + ACL_CAT.transformArguments('dangerous'), + ['ACL', 'CAT', 'dangerous'] + ); }); + }); + + testUtils.testWithClient('client.aclCat', async client => { + const categories = await client.aclCat(); + assert.ok(Array.isArray(categories)); + for (const category of categories) { + assert.equal(typeof category, 'string'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ACL_CAT.ts b/packages/client/lib/commands/ACL_CAT.ts index 161546cfbe9..dd4762239a7 100644 --- a/packages/client/lib/commands/ACL_CAT.ts +++ b/packages/client/lib/commands/ACL_CAT.ts @@ -1,13 +1,16 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(categoryName?: RedisCommandArgument): RedisCommandArguments { - const args: RedisCommandArguments = ['ACL', 'CAT']; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(categoryName?: RedisArgument) { + const args: Array = ['ACL', 'CAT']; if (categoryName) { - args.push(categoryName); + args.push(categoryName); } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_DELUSER.spec.ts b/packages/client/lib/commands/ACL_DELUSER.spec.ts index 5c5ea2fa2a3..d6acbb22230 100644 --- a/packages/client/lib/commands/ACL_DELUSER.spec.ts +++ b/packages/client/lib/commands/ACL_DELUSER.spec.ts @@ -1,30 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ACL_DELUSER'; +import ACL_DELUSER from './ACL_DELUSER'; describe('ACL DELUSER', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('username'), - ['ACL', 'DELUSER', 'username'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + ACL_DELUSER.transformArguments('username'), + ['ACL', 'DELUSER', 'username'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ACL', 'DELUSER', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + ACL_DELUSER.transformArguments(['1', '2']), + ['ACL', 'DELUSER', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.aclDelUser', async client => { - assert.equal( - await client.aclDelUser('dosenotexists'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.aclDelUser', async client => { + assert.equal( + typeof await client.aclDelUser('user'), + 'number' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ACL_DELUSER.ts b/packages/client/lib/commands/ACL_DELUSER.ts index 25ed1a10300..c0f8e15d672 100644 --- a/packages/client/lib/commands/ACL_DELUSER.ts +++ b/packages/client/lib/commands/ACL_DELUSER.ts @@ -1,10 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export function transformArguments( - username: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['ACL', 'DELUSER'], username); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(username: RedisVariadicArgument) { + return pushVariadicArguments(['ACL', 'DELUSER'], username); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_DRYRUN.spec.ts b/packages/client/lib/commands/ACL_DRYRUN.spec.ts index 3154689c29e..519092e0114 100644 --- a/packages/client/lib/commands/ACL_DRYRUN.spec.ts +++ b/packages/client/lib/commands/ACL_DRYRUN.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ACL_DRYRUN'; +import ACL_DRYRUN from './ACL_DRYRUN'; describe('ACL DRYRUN', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('default', ['GET', 'key']), - ['ACL', 'DRYRUN', 'default', 'GET', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ACL_DRYRUN.transformArguments('default', ['GET', 'key']), + ['ACL', 'DRYRUN', 'default', 'GET', 'key'] + ); + }); - testUtils.testWithClient('client.aclDryRun', async client => { - assert.equal( - await client.aclDryRun('default', ['GET', 'key']), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.aclDryRun', async client => { + assert.equal( + await client.aclDryRun('default', ['GET', 'key']), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ACL_DRYRUN.ts b/packages/client/lib/commands/ACL_DRYRUN.ts index 95eed95066c..257f0fe61e2 100644 --- a/packages/client/lib/commands/ACL_DRYRUN.ts +++ b/packages/client/lib/commands/ACL_DRYRUN.ts @@ -1,18 +1,16 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments( - username: RedisCommandArgument, - command: Array -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(username: RedisArgument, command: Array) { return [ - 'ACL', - 'DRYRUN', - username, - ...command + 'ACL', + 'DRYRUN', + username, + ...command ]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_GENPASS.spec.ts b/packages/client/lib/commands/ACL_GENPASS.spec.ts index 3b2a022f972..44c1e167eb7 100644 --- a/packages/client/lib/commands/ACL_GENPASS.spec.ts +++ b/packages/client/lib/commands/ACL_GENPASS.spec.ts @@ -1,23 +1,30 @@ -import { strict as assert } from 'assert'; -import testUtils from '../test-utils'; -import { transformArguments } from './ACL_GENPASS'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ACL_GENPASS from './ACL_GENPASS'; describe('ACL GENPASS', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'GENPASS'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ACL_GENPASS.transformArguments(), + ['ACL', 'GENPASS'] + ); + }); - it('with bits', () => { - assert.deepEqual( - transformArguments(128), - ['ACL', 'GENPASS', '128'] - ); - }); + it('with bits', () => { + assert.deepEqual( + ACL_GENPASS.transformArguments(128), + ['ACL', 'GENPASS', '128'] + ); }); + }); + + testUtils.testWithClient('client.aclGenPass', async client => { + assert.equal( + typeof await client.aclGenPass(), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ACL_GENPASS.ts b/packages/client/lib/commands/ACL_GENPASS.ts index 91a71e220e0..be89ff90a9a 100644 --- a/packages/client/lib/commands/ACL_GENPASS.ts +++ b/packages/client/lib/commands/ACL_GENPASS.ts @@ -1,13 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(bits?: number): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(bits?: number) { const args = ['ACL', 'GENPASS']; if (bits) { - args.push(bits.toString()); + args.push(bits.toString()); } return args; -} + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; -export declare function transformReply(): RedisCommandArgument; diff --git a/packages/client/lib/commands/ACL_GETUSER.spec.ts b/packages/client/lib/commands/ACL_GETUSER.spec.ts index 4cd693db9ce..47351571127 100644 --- a/packages/client/lib/commands/ACL_GETUSER.spec.ts +++ b/packages/client/lib/commands/ACL_GETUSER.spec.ts @@ -1,34 +1,34 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ACL_GETUSER'; +import ACL_GETUSER from './ACL_GETUSER'; describe('ACL GETUSER', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('username'), - ['ACL', 'GETUSER', 'username'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ACL_GETUSER.transformArguments('username'), + ['ACL', 'GETUSER', 'username'] + ); + }); - testUtils.testWithClient('client.aclGetUser', async client => { - const reply = await client.aclGetUser('default'); + testUtils.testWithClient('client.aclGetUser', async client => { + const reply = await client.aclGetUser('default'); - assert.ok(Array.isArray(reply.passwords)); - assert.equal(typeof reply.commands, 'string'); - assert.ok(Array.isArray(reply.flags)); + assert.ok(Array.isArray(reply.passwords)); + assert.equal(typeof reply.commands, 'string'); + assert.ok(Array.isArray(reply.flags)); - if (testUtils.isVersionGreaterThan([7])) { - assert.equal(typeof reply.keys, 'string'); - assert.equal(typeof reply.channels, 'string'); - assert.ok(Array.isArray(reply.selectors)); - } else { - assert.ok(Array.isArray(reply.keys)); + if (testUtils.isVersionGreaterThan([7])) { + assert.equal(typeof reply.keys, 'string'); + assert.equal(typeof reply.channels, 'string'); + assert.ok(Array.isArray(reply.selectors)); + } else { + assert.ok(Array.isArray(reply.keys)); - if (testUtils.isVersionGreaterThan([6, 2])) { - assert.ok(Array.isArray(reply.channels)); - } - } - }, GLOBAL.SERVERS.OPEN); + if (testUtils.isVersionGreaterThan([6, 2])) { + assert.ok(Array.isArray(reply.channels)); + } + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ACL_GETUSER.ts b/packages/client/lib/commands/ACL_GETUSER.ts index 818a945bac1..cbbf48a4c69 100644 --- a/packages/client/lib/commands/ACL_GETUSER.ts +++ b/packages/client/lib/commands/ACL_GETUSER.ts @@ -1,40 +1,43 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -export function transformArguments(username: RedisCommandArgument): RedisCommandArguments { - return ['ACL', 'GETUSER', username]; -} - -type AclGetUserRawReply = [ - 'flags', - Array, - 'passwords', - Array, - 'commands', - RedisCommandArgument, - 'keys', - Array | RedisCommandArgument, - 'channels', - Array | RedisCommandArgument, - 'selectors' | undefined, - Array> | undefined -]; +type AclUser = TuplesToMapReply<[ + [BlobStringReply<'flags'>, ArrayReply], + [BlobStringReply<'passwords'>, ArrayReply], + [BlobStringReply<'commands'>, BlobStringReply], + /** changed to BlobStringReply in 7.0 */ + [BlobStringReply<'keys'>, ArrayReply | BlobStringReply], + /** added in 6.2, changed to BlobStringReply in 7.0 */ + [BlobStringReply<'channels'>, ArrayReply | BlobStringReply], + /** added in 7.0 */ + [BlobStringReply<'selectors'>, ArrayReply, BlobStringReply], + [BlobStringReply<'keys'>, BlobStringReply], + [BlobStringReply<'channels'>, BlobStringReply] + ]>>], +]>; -interface AclUser { - flags: Array; - passwords: Array; - commands: RedisCommandArgument; - keys: Array | RedisCommandArgument; - channels: Array | RedisCommandArgument; - selectors?: Array>; -} - -export function transformReply(reply: AclGetUserRawReply): AclUser { - return { - flags: reply[1], - passwords: reply[3], - commands: reply[5], - keys: reply[7], - channels: reply[9], - selectors: reply[11] - }; -} +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(username: RedisArgument) { + return ['ACL', 'GETUSER', username]; + }, + transformReply: { + 2: (reply: UnwrapReply>) => ({ + flags: reply[1], + passwords: reply[3], + commands: reply[5], + keys: reply[7], + channels: reply[9], + selectors: (reply[11] as unknown as UnwrapReply)?.map(selector => { + const inferred = selector as unknown as UnwrapReply; + return { + commands: inferred[1], + keys: inferred[3], + channels: inferred[5] + }; + }) + }), + 3: undefined as unknown as () => AclUser + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_LIST.spec.ts b/packages/client/lib/commands/ACL_LIST.spec.ts index 9f9156db7a2..b188cae30b0 100644 --- a/packages/client/lib/commands/ACL_LIST.spec.ts +++ b/packages/client/lib/commands/ACL_LIST.spec.ts @@ -1,14 +1,22 @@ -import { strict as assert } from 'assert'; -import testUtils from '../test-utils'; -import { transformArguments } from './ACL_LIST'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ACL_LIST from './ACL_LIST'; describe('ACL LIST', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'LIST'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ACL_LIST.transformArguments(), + ['ACL', 'LIST'] + ); + }); + + testUtils.testWithClient('client.aclList', async client => { + const users = await client.aclList(); + assert.ok(Array.isArray(users)); + for (const user of users) { + assert.equal(typeof user, 'string'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ACL_LIST.ts b/packages/client/lib/commands/ACL_LIST.ts index ae523fe9ce9..1a831a4987c 100644 --- a/packages/client/lib/commands/ACL_LIST.ts +++ b/packages/client/lib/commands/ACL_LIST.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['ACL', 'LIST']; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_LOAD.spec.ts b/packages/client/lib/commands/ACL_LOAD.spec.ts index 703d5eeb252..68552164ce0 100644 --- a/packages/client/lib/commands/ACL_LOAD.spec.ts +++ b/packages/client/lib/commands/ACL_LOAD.spec.ts @@ -1,14 +1,14 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; -import { transformArguments } from './ACL_SAVE'; +import ACL_LOAD from './ACL_LOAD'; -describe('ACL SAVE', () => { - testUtils.isVersionGreaterThanHook([6]); +describe('ACL LOAD', () => { + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'SAVE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ACL_LOAD.transformArguments(), + ['ACL', 'LOAD'] + ); + }); }); diff --git a/packages/client/lib/commands/ACL_LOAD.ts b/packages/client/lib/commands/ACL_LOAD.ts index 88309102b95..39587829b17 100644 --- a/packages/client/lib/commands/ACL_LOAD.ts +++ b/packages/client/lib/commands/ACL_LOAD.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['ACL', 'LOAD']; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_LOG.spec.ts b/packages/client/lib/commands/ACL_LOG.spec.ts index a8296d31da6..b85a7076f4a 100644 --- a/packages/client/lib/commands/ACL_LOG.spec.ts +++ b/packages/client/lib/commands/ACL_LOG.spec.ts @@ -1,53 +1,50 @@ -import { strict as assert } from 'assert'; -import testUtils from '../test-utils'; -import { transformArguments, transformReply } from './ACL_LOG'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ACL_LOG from './ACL_LOG'; describe('ACL LOG', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'LOG'] - ); - }); - - it('with count', () => { - assert.deepEqual( - transformArguments(10), - ['ACL', 'LOG', '10'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ACL_LOG.transformArguments(), + ['ACL', 'LOG'] + ); }); - 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' - }] - ); + it('with count', () => { + assert.deepEqual( + ACL_LOG.transformArguments(10), + ['ACL', 'LOG', '10'] + ); }); + }); + + testUtils.testWithClient('client.aclLog', async client => { + // make sure to create one log + await assert.rejects( + client.auth({ + username: 'incorrect', + password: 'incorrect' + }) + ); + + const logs = await client.aclLog(); + assert.ok(Array.isArray(logs)); + for (const log of logs) { + assert.equal(typeof log.count, 'number'); + assert.equal(typeof log.reason, 'string'); + assert.equal(typeof log.context, 'string'); + assert.equal(typeof log.object, 'string'); + assert.equal(typeof log.username, 'string'); + assert.equal(typeof log['age-seconds'], 'number'); + assert.equal(typeof log['client-info'], 'string'); + if (testUtils.isVersionGreaterThan([7, 2])) { + assert.equal(typeof log['entry-id'], 'number'); + assert.equal(typeof log['timestamp-created'], 'number'); + assert.equal(typeof log['timestamp-last-updated'], 'number'); + } + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ACL_LOG.ts b/packages/client/lib/commands/ACL_LOG.ts index 0fd9aa6f19d..0f0a976e093 100644 --- a/packages/client/lib/commands/ACL_LOG.ts +++ b/packages/client/lib/commands/ACL_LOG.ts @@ -1,50 +1,52 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, TypeMapping } from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; -export function transformArguments(count?: number): RedisCommandArguments { +export type AclLogReply = ArrayReply, NumberReply], + [BlobStringReply<'reason'>, BlobStringReply], + [BlobStringReply<'context'>, BlobStringReply], + [BlobStringReply<'object'>, BlobStringReply], + [BlobStringReply<'username'>, BlobStringReply], + [BlobStringReply<'age-seconds'>, DoubleReply], + [BlobStringReply<'client-info'>, BlobStringReply], + /** added in 7.0 */ + [BlobStringReply<'entry-id'>, NumberReply], + /** added in 7.0 */ + [BlobStringReply<'timestamp-created'>, NumberReply], + /** added in 7.0 */ + [BlobStringReply<'timestamp-last-updated'>, NumberReply] +]>>; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(count?: number) { const args = ['ACL', 'LOG']; - if (count) { - args.push(count.toString()); + if (count !== undefined) { + args.push(count.toString()); } return args; -} - -type AclLogRawReply = [ - _: RedisCommandArgument, - count: number, - _: RedisCommandArgument, - reason: RedisCommandArgument, - _: RedisCommandArgument, - context: RedisCommandArgument, - _: RedisCommandArgument, - object: RedisCommandArgument, - _: RedisCommandArgument, - username: RedisCommandArgument, - _: RedisCommandArgument, - ageSeconds: RedisCommandArgument, - _: RedisCommandArgument, - clientInfo: RedisCommandArgument -]; - -interface AclLog { - count: number; - reason: RedisCommandArgument; - context: RedisCommandArgument; - object: RedisCommandArgument; - username: RedisCommandArgument; - ageSeconds: number; - clientInfo: RedisCommandArgument; -} - -export function transformReply(reply: Array): 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] - })); -} + }, + transformReply: { + 2: (reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { + return reply.map(item => { + const inferred = item as unknown as UnwrapReply; + return { + count: inferred[1], + reason: inferred[3], + context: inferred[5], + object: inferred[7], + username: inferred[9], + 'age-seconds': transformDoubleReply[2](inferred[11], preserve, typeMapping), + 'client-info': inferred[13], + 'entry-id': inferred[15], + 'timestamp-created': inferred[17], + 'timestamp-last-updated': inferred[19] + }; + }) + }, + 3: undefined as unknown as () => AclLogReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_LOG_RESET.spec.ts b/packages/client/lib/commands/ACL_LOG_RESET.spec.ts index 5d26e45d04f..8849440c1a6 100644 --- a/packages/client/lib/commands/ACL_LOG_RESET.spec.ts +++ b/packages/client/lib/commands/ACL_LOG_RESET.spec.ts @@ -1,14 +1,21 @@ -import { strict as assert } from 'assert'; -import testUtils from '../test-utils'; -import { transformArguments } from './ACL_LOG_RESET'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ACL_LOG_RESET from './ACL_LOG_RESET'; describe('ACL LOG RESET', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'LOG', 'RESET'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ACL_LOG_RESET.transformArguments(), + ['ACL', 'LOG', 'RESET'] + ); + }); + + testUtils.testWithClient('client.aclLogReset', async client => { + assert.equal( + await client.aclLogReset(), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ACL_LOG_RESET.ts b/packages/client/lib/commands/ACL_LOG_RESET.ts index 8ff0be4f8b9..91d58d538e9 100644 --- a/packages/client/lib/commands/ACL_LOG_RESET.ts +++ b/packages/client/lib/commands/ACL_LOG_RESET.ts @@ -1,7 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; +import ACL_LOG from './ACL_LOG'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: ACL_LOG.IS_READ_ONLY, + transformArguments() { return ['ACL', 'LOG', 'RESET']; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_SAVE.spec.ts b/packages/client/lib/commands/ACL_SAVE.spec.ts index f4de312bb7a..1fe402867e7 100644 --- a/packages/client/lib/commands/ACL_SAVE.spec.ts +++ b/packages/client/lib/commands/ACL_SAVE.spec.ts @@ -1,14 +1,14 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; -import { transformArguments } from './ACL_LOAD'; +import ACL_SAVE from './ACL_SAVE'; -describe('ACL LOAD', () => { - testUtils.isVersionGreaterThanHook([6]); +describe('ACL SAVE', () => { + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'LOAD'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ACL_SAVE.transformArguments(), + ['ACL', 'SAVE'] + ); + }); }); diff --git a/packages/client/lib/commands/ACL_SAVE.ts b/packages/client/lib/commands/ACL_SAVE.ts index e57cd697297..8c2e2dab115 100644 --- a/packages/client/lib/commands/ACL_SAVE.ts +++ b/packages/client/lib/commands/ACL_SAVE.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['ACL', 'SAVE']; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_SETUSER.spec.ts b/packages/client/lib/commands/ACL_SETUSER.spec.ts index 9c8ea8a59e0..10aea62ed02 100644 --- a/packages/client/lib/commands/ACL_SETUSER.spec.ts +++ b/packages/client/lib/commands/ACL_SETUSER.spec.ts @@ -1,23 +1,23 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; -import { transformArguments } from './ACL_SETUSER'; +import ACL_SETUSER from './ACL_SETUSER'; describe('ACL SETUSER', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('username', 'allkeys'), - ['ACL', 'SETUSER', 'username', 'allkeys'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + ACL_SETUSER.transformArguments('username', 'allkeys'), + ['ACL', 'SETUSER', 'username', 'allkeys'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('username', ['allkeys', 'allchannels']), - ['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels'] - ); - }); + it('array', () => { + assert.deepEqual( + ACL_SETUSER.transformArguments('username', ['allkeys', 'allchannels']), + ['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels'] + ); }); + }); }); diff --git a/packages/client/lib/commands/ACL_SETUSER.ts b/packages/client/lib/commands/ACL_SETUSER.ts index a12cc8ed24e..c99fec3d9ba 100644 --- a/packages/client/lib/commands/ACL_SETUSER.ts +++ b/packages/client/lib/commands/ACL_SETUSER.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export function transformArguments( - username: RedisCommandArgument, - rule: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['ACL', 'SETUSER', username], rule); -} - -export declare function transformReply(): RedisCommandArgument; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(username: RedisArgument, rule: RedisVariadicArgument) { + return pushVariadicArguments(['ACL', 'SETUSER', username], rule); + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_USERS.spec.ts b/packages/client/lib/commands/ACL_USERS.spec.ts index 35e06ce8494..2d433d4ec1e 100644 --- a/packages/client/lib/commands/ACL_USERS.spec.ts +++ b/packages/client/lib/commands/ACL_USERS.spec.ts @@ -1,14 +1,14 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; -import { transformArguments } from './ACL_USERS'; +import ACL_USERS from './ACL_USERS'; describe('ACL USERS', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'USERS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ACL_USERS.transformArguments(), + ['ACL', 'USERS'] + ); + }); }); diff --git a/packages/client/lib/commands/ACL_USERS.ts b/packages/client/lib/commands/ACL_USERS.ts index 7970a262e26..ee8c619f25f 100644 --- a/packages/client/lib/commands/ACL_USERS.ts +++ b/packages/client/lib/commands/ACL_USERS.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['ACL', 'USERS']; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_WHOAMI.spec.ts b/packages/client/lib/commands/ACL_WHOAMI.spec.ts index 32eb327beea..24a5cbd1d63 100644 --- a/packages/client/lib/commands/ACL_WHOAMI.spec.ts +++ b/packages/client/lib/commands/ACL_WHOAMI.spec.ts @@ -1,14 +1,14 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; -import { transformArguments } from './ACL_WHOAMI'; +import ACL_WHOAMI from './ACL_WHOAMI'; describe('ACL WHOAMI', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ACL', 'WHOAMI'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ACL_WHOAMI.transformArguments(), + ['ACL', 'WHOAMI'] + ); + }); }); diff --git a/packages/client/lib/commands/ACL_WHOAMI.ts b/packages/client/lib/commands/ACL_WHOAMI.ts index 3c41171638e..81a1c84a3c6 100644 --- a/packages/client/lib/commands/ACL_WHOAMI.ts +++ b/packages/client/lib/commands/ACL_WHOAMI.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['ACL', 'WHOAMI']; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/APPEND.spec.ts b/packages/client/lib/commands/APPEND.spec.ts index 23353866843..ca18a00ac42 100644 --- a/packages/client/lib/commands/APPEND.spec.ts +++ b/packages/client/lib/commands/APPEND.spec.ts @@ -1,11 +1,22 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './APPEND'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import APPEND from './APPEND'; describe('APPEND', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'value'), - ['APPEND', 'key', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + APPEND.transformArguments('key', 'value'), + ['APPEND', 'key', 'value'] + ); + }); + + testUtils.testAll('append', async client => { + assert.equal( + await client.append('key', 'value'), + 5 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/APPEND.ts b/packages/client/lib/commands/APPEND.ts index 66f7fc84798..1bc01024998 100644 --- a/packages/client/lib/commands/APPEND.ts +++ b/packages/client/lib/commands/APPEND.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - value: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, value: RedisArgument) { return ['APPEND', key, value]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ASKING.spec.ts b/packages/client/lib/commands/ASKING.spec.ts index 3da2015199e..bd83bec599f 100644 --- a/packages/client/lib/commands/ASKING.spec.ts +++ b/packages/client/lib/commands/ASKING.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './ASKING'; +import { strict as assert } from 'node:assert'; +import ASKING from './ASKING'; describe('ASKING', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ASKING'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ASKING.transformArguments(), + ['ASKING'] + ); + }); }); diff --git a/packages/client/lib/commands/ASKING.ts b/packages/client/lib/commands/ASKING.ts index 8a87806fe62..c6ada477ee4 100644 --- a/packages/client/lib/commands/ASKING.ts +++ b/packages/client/lib/commands/ASKING.ts @@ -1,7 +1,10 @@ -import { RedisCommandArguments, RedisCommandArgument } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['ASKING']; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/AUTH.spec.ts b/packages/client/lib/commands/AUTH.spec.ts index 1907488346e..2da016ba873 100644 --- a/packages/client/lib/commands/AUTH.spec.ts +++ b/packages/client/lib/commands/AUTH.spec.ts @@ -1,25 +1,25 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './AUTH'; +import { strict as assert } from 'node:assert'; +import AUTH from './AUTH'; describe('AUTH', () => { - describe('transformArguments', () => { - it('password only', () => { - assert.deepEqual( - transformArguments({ - password: 'password' - }), - ['AUTH', 'password'] - ); - }); + describe('transformArguments', () => { + it('password only', () => { + assert.deepEqual( + AUTH.transformArguments({ + password: 'password' + }), + ['AUTH', 'password'] + ); + }); - it('username & password', () => { - assert.deepEqual( - transformArguments({ - username: 'username', - password: 'password' - }), - ['AUTH', 'username', 'password'] - ); - }); + it('username & password', () => { + assert.deepEqual( + AUTH.transformArguments({ + username: 'username', + password: 'password' + }), + ['AUTH', 'username', 'password'] + ); }); + }); }); diff --git a/packages/client/lib/commands/AUTH.ts b/packages/client/lib/commands/AUTH.ts index 49b0df6d313..4c7a0b0c765 100644 --- a/packages/client/lib/commands/AUTH.ts +++ b/packages/client/lib/commands/AUTH.ts @@ -1,16 +1,23 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export interface AuthOptions { - username?: RedisCommandArgument; - password: RedisCommandArgument; + username?: RedisArgument; + password: RedisArgument; } -export function transformArguments({ username, password }: AuthOptions): RedisCommandArguments { - if (!username) { - return ['AUTH', password]; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments({ username, password }: AuthOptions) { + const args: Array = ['AUTH']; + + if (username !== undefined) { + args.push(username); } - return ['AUTH', username, password]; -} + args.push(password); -export declare function transformReply(): RedisCommandArgument; + return args; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/BGREWRITEAOF.spec.ts b/packages/client/lib/commands/BGREWRITEAOF.spec.ts index d0e150e155b..5447fc70a79 100644 --- a/packages/client/lib/commands/BGREWRITEAOF.spec.ts +++ b/packages/client/lib/commands/BGREWRITEAOF.spec.ts @@ -1,11 +1,19 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './BGREWRITEAOF'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import BGREWRITEAOF from './BGREWRITEAOF'; describe('BGREWRITEAOF', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['BGREWRITEAOF'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + BGREWRITEAOF.transformArguments(), + ['BGREWRITEAOF'] + ); + }); + + testUtils.testWithClient('client.bgRewriteAof', async client => { + assert.equal( + typeof await client.bgRewriteAof(), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/BGREWRITEAOF.ts b/packages/client/lib/commands/BGREWRITEAOF.ts index be4ec2546ab..5f9a46e318f 100644 --- a/packages/client/lib/commands/BGREWRITEAOF.ts +++ b/packages/client/lib/commands/BGREWRITEAOF.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['BGREWRITEAOF']; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BGSAVE.spec.ts b/packages/client/lib/commands/BGSAVE.spec.ts index 8e4de5eef5b..7944722dd56 100644 --- a/packages/client/lib/commands/BGSAVE.spec.ts +++ b/packages/client/lib/commands/BGSAVE.spec.ts @@ -1,23 +1,32 @@ -import { strict as assert } from 'assert'; -import { describe } from 'mocha'; -import { transformArguments } from './BGSAVE'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import BGSAVE from './BGSAVE'; describe('BGSAVE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['BGSAVE'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + BGSAVE.transformArguments(), + ['BGSAVE'] + ); + }); - it('with SCHEDULE', () => { - assert.deepEqual( - transformArguments({ - SCHEDULE: true - }), - ['BGSAVE', 'SCHEDULE'] - ); - }); + it('with SCHEDULE', () => { + assert.deepEqual( + BGSAVE.transformArguments({ + SCHEDULE: true + }), + ['BGSAVE', 'SCHEDULE'] + ); }); + }); + + testUtils.testWithClient('client.bgSave', async client => { + assert.equal( + typeof await client.bgSave({ + SCHEDULE: true // using `SCHEDULE` to make sure it won't throw an error + }), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/BGSAVE.ts b/packages/client/lib/commands/BGSAVE.ts index 9c90f3485be..dc0f5056708 100644 --- a/packages/client/lib/commands/BGSAVE.ts +++ b/packages/client/lib/commands/BGSAVE.ts @@ -1,17 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -interface BgSaveOptions { - SCHEDULE?: true; +export interface BgSaveOptions { + SCHEDULE?: boolean; } -export function transformArguments(options?: BgSaveOptions): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(options?: BgSaveOptions) { const args = ['BGSAVE']; - + if (options?.SCHEDULE) { - args.push('SCHEDULE'); + args.push('SCHEDULE'); } return args; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BITCOUNT.spec.ts b/packages/client/lib/commands/BITCOUNT.spec.ts index 76e7b03f7c9..ceb6476a31c 100644 --- a/packages/client/lib/commands/BITCOUNT.spec.ts +++ b/packages/client/lib/commands/BITCOUNT.spec.ts @@ -1,44 +1,47 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './BITCOUNT'; +import BITCOUNT from './BITCOUNT'; describe('BITCOUNT', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key'), - ['BITCOUNT', 'key'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + BITCOUNT.transformArguments('key'), + ['BITCOUNT', 'key'] + ); + }); - describe('with range', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', { - start: 0, - end: 1 - }), - ['BITCOUNT', 'key', '0', '1'] - ); - }); + describe('with range', () => { + it('simple', () => { + assert.deepEqual( + BITCOUNT.transformArguments('key', { + start: 0, + end: 1 + }), + ['BITCOUNT', 'key', '0', '1'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments('key', { - start: 0, - end: 1, - mode: 'BIT' - }), - ['BITCOUNT', 'key', '0', '1', 'BIT'] - ); - }); - }); + it('with mode', () => { + assert.deepEqual( + BITCOUNT.transformArguments('key', { + start: 0, + end: 1, + mode: 'BIT' + }), + ['BITCOUNT', 'key', '0', '1', 'BIT'] + ); + }); }); + }); - testUtils.testWithClient('client.bitCount', async client => { - assert.equal( - await client.bitCount('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('bitCount', async client => { + assert.equal( + await client.bitCount('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BITCOUNT.ts b/packages/client/lib/commands/BITCOUNT.ts index 4bbd4f00911..6ec6b89be8b 100644 --- a/packages/client/lib/commands/BITCOUNT.ts +++ b/packages/client/lib/commands/BITCOUNT.ts @@ -1,33 +1,29 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -interface BitCountRange { - start: number; - end: number; - mode?: 'BYTE' | 'BIT'; +export interface BitCountRange { + start: number; + end: number; + mode?: 'BYTE' | 'BIT'; } -export function transformArguments( - key: RedisCommandArgument, - range?: BitCountRange -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, range?: BitCountRange) { const args = ['BITCOUNT', key]; if (range) { - args.push( - range.start.toString(), - range.end.toString() - ); - - if (range.mode) { - args.push(range.mode); - } + args.push( + range.start.toString(), + range.end.toString() + ); + + if (range.mode) { + args.push(range.mode); + } } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BITFIELD.spec.ts b/packages/client/lib/commands/BITFIELD.spec.ts index aaf0f93e501..7f805755493 100644 --- a/packages/client/lib/commands/BITFIELD.spec.ts +++ b/packages/client/lib/commands/BITFIELD.spec.ts @@ -1,46 +1,55 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './BITFIELD'; +import BITFIELD from './BITFIELD'; describe('BITFIELD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [{ - operation: 'OVERFLOW', - behavior: 'WRAP' - }, { - operation: 'GET', - encoding: 'i8', - offset: 0 - }, { - operation: 'OVERFLOW', - behavior: 'SAT' - }, { - operation: 'SET', - encoding: 'i16', - offset: 1, - value: 0 - }, { - operation: 'OVERFLOW', - behavior: 'FAIL' - }, { - operation: 'INCRBY', - encoding: 'i32', - offset: 2, - increment: 1 - }]), - ['BITFIELD', 'key', 'OVERFLOW', 'WRAP', 'GET', 'i8', '0', 'OVERFLOW', 'SAT', 'SET', 'i16', '1', '0', 'OVERFLOW', 'FAIL', 'INCRBY', 'i32', '2', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + BITFIELD.transformArguments('key', [{ + operation: 'OVERFLOW', + behavior: 'WRAP' + }, { + operation: 'GET', + encoding: 'i8', + offset: 0 + }, { + operation: 'OVERFLOW', + behavior: 'SAT' + }, { + operation: 'SET', + encoding: 'i16', + offset: 1, + value: 0 + }, { + operation: 'OVERFLOW', + behavior: 'FAIL' + }, { + operation: 'INCRBY', + encoding: 'i32', + offset: 2, + increment: 1 + }]), + ['BITFIELD', 'key', 'OVERFLOW', 'WRAP', 'GET', 'i8', '0', 'OVERFLOW', 'SAT', 'SET', 'i16', '1', '0', 'OVERFLOW', 'FAIL', 'INCRBY', 'i32', '2', '1'] + ); + }); - testUtils.testWithClient('client.bitField', async client => { - assert.deepEqual( - await client.bitField('key', [{ - operation: 'GET', - encoding: 'i8', - offset: 0 - }]), - [0] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('bitField', async client => { + const a = client.bitField('key', [{ + operation: 'GET', + encoding: 'i8', + offset: 0 + }]); + + assert.deepEqual( + await client.bitField('key', [{ + operation: 'GET', + encoding: 'i8', + offset: 0 + }]), + [0] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BITFIELD.ts b/packages/client/lib/commands/BITFIELD.ts index 6a477b89f01..5d7d4bf7282 100644 --- a/packages/client/lib/commands/BITFIELD.ts +++ b/packages/client/lib/commands/BITFIELD.ts @@ -1,80 +1,87 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '../RESP/types'; export type BitFieldEncoding = `${'i' | 'u'}${number}`; export interface BitFieldOperation { - operation: S; + operation: S; } export interface BitFieldGetOperation extends BitFieldOperation<'GET'> { - encoding: BitFieldEncoding; - offset: number | string; + encoding: BitFieldEncoding; + offset: number | string; } -interface BitFieldSetOperation extends BitFieldOperation<'SET'> { - encoding: BitFieldEncoding; - offset: number | string; - value: number; +export interface BitFieldSetOperation extends BitFieldOperation<'SET'> { + encoding: BitFieldEncoding; + offset: number | string; + value: number; } -interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> { - encoding: BitFieldEncoding; - offset: number | string; - increment: number; +export interface BitFieldIncrByOperation extends BitFieldOperation<'INCRBY'> { + encoding: BitFieldEncoding; + offset: number | string; + increment: number; } -interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> { - behavior: string; +export interface BitFieldOverflowOperation extends BitFieldOperation<'OVERFLOW'> { + behavior: string; } -type BitFieldOperations = Array< - BitFieldGetOperation | - BitFieldSetOperation | - BitFieldIncrByOperation | - BitFieldOverflowOperation +export type BitFieldOperations = Array< + BitFieldGetOperation | + BitFieldSetOperation | + BitFieldIncrByOperation | + BitFieldOverflowOperation >; -export function transformArguments(key: string, operations: BitFieldOperations): Array { +export type BitFieldRoOperations = Array< + Omit +>; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, operations: BitFieldOperations) { const args = ['BITFIELD', key]; for (const options of operations) { - switch (options.operation) { - case 'GET': - args.push( - 'GET', - options.encoding, - options.offset.toString() - ); - break; + switch (options.operation) { + case 'GET': + args.push( + 'GET', + options.encoding, + options.offset.toString() + ); + break; - case 'SET': - args.push( - 'SET', - options.encoding, - options.offset.toString(), - options.value.toString() - ); - break; + case 'SET': + args.push( + 'SET', + options.encoding, + options.offset.toString(), + options.value.toString() + ); + break; - case 'INCRBY': - args.push( - 'INCRBY', - options.encoding, - options.offset.toString(), - options.increment.toString() - ); - break; + case 'INCRBY': + args.push( + 'INCRBY', + options.encoding, + options.offset.toString(), + options.increment.toString() + ); + break; - case 'OVERFLOW': - args.push( - 'OVERFLOW', - options.behavior - ); - break; - } + case 'OVERFLOW': + args.push( + 'OVERFLOW', + options.behavior + ); + break; + } } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BITFIELD_RO.spec.ts b/packages/client/lib/commands/BITFIELD_RO.spec.ts index 98399d5f235..0793100193f 100644 --- a/packages/client/lib/commands/BITFIELD_RO.spec.ts +++ b/packages/client/lib/commands/BITFIELD_RO.spec.ts @@ -1,27 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './BITFIELD_RO'; +import BITFIELD_RO from './BITFIELD_RO'; -describe('BITFIELD RO', () => { - testUtils.isVersionGreaterThanHook([6, 2]); +describe('BITFIELD_RO', () => { + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', [{ - encoding: 'i8', - offset: 0 - }]), - ['BITFIELD_RO', 'key', 'GET', 'i8', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + BITFIELD_RO.transformArguments('key', [{ + encoding: 'i8', + offset: 0 + }]), + ['BITFIELD_RO', 'key', 'GET', 'i8', '0'] + ); + }); - testUtils.testWithClient('client.bitFieldRo', async client => { - assert.deepEqual( - await client.bitFieldRo('key', [{ - encoding: 'i8', - offset: 0 - }]), - [0] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('bitFieldRo', async client => { + assert.deepEqual( + await client.bitFieldRo('key', [{ + encoding: 'i8', + offset: 0 + }]), + [0] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BITFIELD_RO.ts b/packages/client/lib/commands/BITFIELD_RO.ts index efd4eac188e..99e500abc04 100644 --- a/packages/client/lib/commands/BITFIELD_RO.ts +++ b/packages/client/lib/commands/BITFIELD_RO.ts @@ -1,26 +1,25 @@ +import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; import { BitFieldGetOperation } from './BITFIELD'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -type BitFieldRoOperations = Array< - Omit & - Partial> +export type BitFieldRoOperations = Array< + Omit >; -export function transformArguments(key: string, operations: BitFieldRoOperations): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, operations: BitFieldRoOperations) { const args = ['BITFIELD_RO', key]; for (const operation of operations) { - args.push( - 'GET', - operation.encoding, - operation.offset.toString() - ); + args.push( + 'GET', + operation.encoding, + operation.offset.toString() + ); } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BITOP.spec.ts b/packages/client/lib/commands/BITOP.spec.ts index 554530d56f4..4df1782467f 100644 --- a/packages/client/lib/commands/BITOP.spec.ts +++ b/packages/client/lib/commands/BITOP.spec.ts @@ -1,35 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './BITOP'; +import BITOP from './BITOP'; describe('BITOP', () => { - describe('transformArguments', () => { - it('single key', () => { - assert.deepEqual( - transformArguments('AND', 'destKey', 'key'), - ['BITOP', 'AND', 'destKey', 'key'] - ); - }); - - it('multiple keys', () => { - assert.deepEqual( - transformArguments('AND', 'destKey', ['1', '2']), - ['BITOP', 'AND', 'destKey', '1', '2'] - ); - }); + describe('transformArguments', () => { + it('single key', () => { + assert.deepEqual( + BITOP.transformArguments('AND', 'destKey', 'key'), + ['BITOP', 'AND', 'destKey', 'key'] + ); }); - testUtils.testWithClient('client.bitOp', async client => { - assert.equal( - await client.bitOp('AND', 'destKey', 'key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + it('multiple keys', () => { + assert.deepEqual( + BITOP.transformArguments('AND', 'destKey', ['1', '2']), + ['BITOP', 'AND', 'destKey', '1', '2'] + ); + }); + }); - testUtils.testWithCluster('cluster.bitOp', async cluster => { - assert.equal( - await cluster.bitOp('AND', '{tag}destKey', '{tag}key'), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('bitOp', async client => { + assert.equal( + await client.bitOp('AND', '{tag}destKey', '{tag}key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BITOP.ts b/packages/client/lib/commands/BITOP.ts index e2953303d41..4c34845699e 100644 --- a/packages/client/lib/commands/BITOP.ts +++ b/packages/client/lib/commands/BITOP.ts @@ -1,16 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 2; +export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT'; -type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT'; - -export function transformArguments( +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments( operation: BitOperations, - destKey: RedisCommandArgument, - key: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['BITOP', operation, destKey], key); -} - -export declare function transformReply(): number; + destKey: RedisArgument, + key: RedisVariadicArgument + ) { + return pushVariadicArguments(['BITOP', operation, destKey], key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BITPOS.spec.ts b/packages/client/lib/commands/BITPOS.spec.ts index 2a0758fe5da..61940560057 100644 --- a/packages/client/lib/commands/BITPOS.spec.ts +++ b/packages/client/lib/commands/BITPOS.spec.ts @@ -1,49 +1,45 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './BITPOS'; +import BITPOS 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'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + BITPOS.transformArguments('key', 1), + ['BITPOS', 'key', '1'] + ); + }); - it('with start and end', () => { - assert.deepEqual( - transformArguments('key', 1, 1, -1), - ['BITPOS', 'key', '1', '1', '-1'] - ); - }); + it('with start', () => { + assert.deepEqual( + BITPOS.transformArguments('key', 1, 1), + ['BITPOS', 'key', '1', '1'] + ); + }); - it('with start, end and mode', () => { - assert.deepEqual( - transformArguments('key', 1, 1, -1, 'BIT'), - ['BITPOS', 'key', '1', '1', '-1', 'BIT'] - ); - }); + it('with start and end', () => { + assert.deepEqual( + BITPOS.transformArguments('key', 1, 1, -1), + ['BITPOS', 'key', '1', '1', '-1'] + ); }); - testUtils.testWithClient('client.bitPos', async client => { - assert.equal( - await client.bitPos('key', 1, 1), - -1 - ); - }, GLOBAL.SERVERS.OPEN); + it('with start, end and mode', () => { + assert.deepEqual( + BITPOS.transformArguments('key', 1, 1, -1, 'BIT'), + ['BITPOS', 'key', '1', '1', '-1', 'BIT'] + ); + }); + }); - testUtils.testWithCluster('cluster.bitPos', async cluster => { - assert.equal( - await cluster.bitPos('key', 1, 1), - -1 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('bitPos', async client => { + assert.equal( + await client.bitPos('key', 1, 1), + -1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BITPOS.ts b/packages/client/lib/commands/BITPOS.ts index a9a035fd9f2..5d6276dffc0 100644 --- a/packages/client/lib/commands/BITPOS.ts +++ b/packages/client/lib/commands/BITPOS.ts @@ -1,32 +1,31 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { BitValue } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, bit: BitValue, start?: number, end?: number, mode?: 'BYTE' | 'BIT' -): RedisCommandArguments { + ) { const args = ['BITPOS', key, bit.toString()]; - if (typeof start === 'number') { - args.push(start.toString()); + if (start !== undefined) { + args.push(start.toString()); } - if (typeof end === 'number') { - args.push(end.toString()); + if (end !== undefined) { + args.push(end.toString()); } if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BLMOVE.spec.ts b/packages/client/lib/commands/BLMOVE.spec.ts index 3b86c1ec91e..0eca8c61005 100644 --- a/packages/client/lib/commands/BLMOVE.spec.ts +++ b/packages/client/lib/commands/BLMOVE.spec.ts @@ -1,43 +1,35 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './BLMOVE'; -import { commandOptions } from '../../index'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; +import BLMOVE from './BLMOVE'; describe('BLMOVE', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0), - ['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + BLMOVE.transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0), + ['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0'] + ); + }); - testUtils.testWithClient('client.blMove', async client => { - const [blMoveReply] = await Promise.all([ - client.blMove(commandOptions({ - isolated: true - }), 'source', 'destination', 'LEFT', 'RIGHT', 0), - client.lPush('source', 'element') - ]); + testUtils.testAll('blMove - null', async client => { + assert.equal( + await client.blMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT', BLOCKING_MIN_VALUE), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); - assert.equal( - blMoveReply, - 'element' - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.blMove', async cluster => { - const [blMoveReply] = await Promise.all([ - cluster.blMove(commandOptions({ - isolated: true - }), '{tag}source', '{tag}destination', 'LEFT', 'RIGHT', 0), - cluster.lPush('{tag}source', 'element') - ]); - - assert.equal( - blMoveReply, - 'element' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('blMove - with member', async client => { + const [, reply] = await Promise.all([ + client.lPush('{tag}source', 'element'), + client.blMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT', BLOCKING_MIN_VALUE) + ]); + assert.equal(reply, 'element'); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BLMOVE.ts b/packages/client/lib/commands/BLMOVE.ts index ee808e70fcc..c7e4844375f 100644 --- a/packages/client/lib/commands/BLMOVE.ts +++ b/packages/client/lib/commands/BLMOVE.ts @@ -1,23 +1,24 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; import { ListSide } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument, - sourceDirection: ListSide, - destinationDirection: ListSide, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + source: RedisArgument, + destination: RedisArgument, + sourceSide: ListSide, + destinationSide: ListSide, timeout: number -): RedisCommandArguments { + ) { return [ - 'BLMOVE', - source, - destination, - sourceDirection, - destinationDirection, - timeout.toString() + 'BLMOVE', + source, + destination, + sourceSide, + destinationSide, + timeout.toString() ]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BLMPOP.spec.ts b/packages/client/lib/commands/BLMPOP.spec.ts index 15853a771b0..b40556b1e46 100644 --- a/packages/client/lib/commands/BLMPOP.spec.ts +++ b/packages/client/lib/commands/BLMPOP.spec.ts @@ -1,32 +1,49 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './BLMPOP'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; +import BLMPOP from './BLMPOP'; describe('BLMPOP', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(0, 'key', 'LEFT'), - ['BLMPOP', '0', '1', 'key', 'LEFT'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + BLMPOP.transformArguments(0, 'key', 'LEFT'), + ['BLMPOP', '0', '1', 'key', 'LEFT'] + ); + }); - it('with COUNT', () => { - assert.deepEqual( - transformArguments(0, 'key', 'LEFT', { - COUNT: 2 - }), - ['BLMPOP', '0', '1', 'key', 'LEFT', 'COUNT', '2'] - ); - }); + it('with COUNT', () => { + assert.deepEqual( + BLMPOP.transformArguments(0, 'key', 'LEFT', { + COUNT: 1 + }), + ['BLMPOP', '0', '1', 'key', 'LEFT', 'COUNT', '1'] + ); }); + }); + + testUtils.testAll('blmPop - null', async client => { + assert.equal( + await client.blmPop(BLOCKING_MIN_VALUE, 'key', 'RIGHT'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); - testUtils.testWithClient('client.blmPop', async client => { - assert.deepEqual( - await client.blmPop(1, 'key', 'RIGHT'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('blmPop - with member', async client => { + const [, reply] = await Promise.all([ + client.lPush('key', 'element'), + client.blmPop(BLOCKING_MIN_VALUE, 'key', 'RIGHT') + ]); + assert.deepEqual(reply, [ + 'key', + ['element'] + ]); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BLMPOP.ts b/packages/client/lib/commands/BLMPOP.ts index 11bfad8b99b..3122e908600 100644 --- a/packages/client/lib/commands/BLMPOP.ts +++ b/packages/client/lib/commands/BLMPOP.ts @@ -1,20 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformLMPopArguments, LMPopOptions, ListSide } from './generic-transformers'; +import { Command } from '../RESP/types'; +import LMPOP, { LMPopArguments, transformLMPopArguments } from './LMPOP'; -export const FIRST_KEY_INDEX = 3; - -export function transformArguments( +export default { + FIRST_KEY_INDEX: 3, + IS_READ_ONLY: false, + transformArguments( timeout: number, - keys: RedisCommandArgument | Array, - side: ListSide, - options?: LMPopOptions -): RedisCommandArguments { + ...args: LMPopArguments + ) { return transformLMPopArguments( - ['BLMPOP', timeout.toString()], - keys, - side, - options + ['BLMPOP', timeout.toString()], + ...args ); -} - -export { transformReply } from './LMPOP'; + }, + transformReply: LMPOP.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BLPOP.spec.ts b/packages/client/lib/commands/BLPOP.spec.ts index 84920c851e1..4bcc08d0fc9 100644 --- a/packages/client/lib/commands/BLPOP.spec.ts +++ b/packages/client/lib/commands/BLPOP.spec.ts @@ -1,79 +1,46 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './BLPOP'; -import { commandOptions } from '../../index'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; +import BLPOP from './BLPOP'; 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('transformArguments', () => { + it('single', () => { + assert.deepEqual( + BLPOP.transformArguments('key', 0), + ['BLPOP', 'key', '0'] + ); }); - describe('transformReply', () => { - it('null', () => { - assert.equal( - transformReply(null), - null - ); - }); - - it('member', () => { - assert.deepEqual( - transformReply(['key', 'element']), - { - key: 'key', - element: 'element' - } - ); - }); + it('multiple', () => { + assert.deepEqual( + BLPOP.transformArguments(['1', '2'], 0), + ['BLPOP', '1', '2', '0'] + ); }); - - testUtils.testWithClient('client.blPop', async client => { - const [ blPopReply ] = await Promise.all([ - client.blPop( - commandOptions({ isolated: true }), - 'key', - 1 - ), - client.lPush('key', 'element'), - ]); - - assert.deepEqual( - blPopReply, - { - key: 'key', - element: 'element' - } - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.blPop', async cluster => { - const [ blPopReply ] = await Promise.all([ - cluster.blPop( - commandOptions({ isolated: true }), - 'key', - 1 - ), - cluster.lPush('key', 'element') - ]); - - assert.deepEqual( - blPopReply, - { - key: 'key', - element: 'element' - } - ); - }, GLOBAL.CLUSTERS.OPEN); + }); + + testUtils.testAll('blPop - null', async client => { + assert.equal( + await client.blPop('key', BLOCKING_MIN_VALUE), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testAll('blPop - with member', async client => { + const [, reply] = await Promise.all([ + client.lPush('key', 'element'), + client.blPop('key', 1) + ]); + + assert.deepEqual(reply, { + key: 'key', + element: 'element' + }); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BLPOP.ts b/packages/client/lib/commands/BLPOP.ts index 46ef41ad6f0..c9f8b4775eb 100644 --- a/packages/client/lib/commands/BLPOP.ts +++ b/packages/client/lib/commands/BLPOP.ts @@ -1,31 +1,23 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - keys: RedisCommandArgument | Array, +import { UnwrapReply, NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisVariadicArgument, timeout: number -): RedisCommandArguments { - const args = pushVerdictArguments(['BLPOP'], keys); - + ) { + const args = pushVariadicArguments(['BLPOP'], key); args.push(timeout.toString()); - return args; -} - -type BLPopRawReply = null | [RedisCommandArgument, RedisCommandArgument]; - -type BLPopReply = null | { - key: RedisCommandArgument; - element: RedisCommandArgument; -}; - -export function transformReply(reply: BLPopRawReply): BLPopReply { + }, + transformReply(reply: UnwrapReply>) { if (reply === null) return null; return { - key: reply[0], - element: reply[1] + key: reply[0], + element: reply[1] }; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/BRPOP.spec.ts b/packages/client/lib/commands/BRPOP.spec.ts index fc203e1abdf..21631d763f4 100644 --- a/packages/client/lib/commands/BRPOP.spec.ts +++ b/packages/client/lib/commands/BRPOP.spec.ts @@ -1,79 +1,46 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './BRPOP'; -import { commandOptions } from '../../index'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; +import BRPOP from './BRPOP'; 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('transformArguments', () => { + it('single', () => { + assert.deepEqual( + BRPOP.transformArguments('key', 0), + ['BRPOP', 'key', '0'] + ); }); - describe('transformReply', () => { - it('null', () => { - assert.equal( - transformReply(null), - null - ); - }); - - it('member', () => { - assert.deepEqual( - transformReply(['key', 'element']), - { - key: 'key', - element: 'element' - } - ); - }); + it('multiple', () => { + assert.deepEqual( + BRPOP.transformArguments(['1', '2'], 0), + ['BRPOP', '1', '2', '0'] + ); }); - - testUtils.testWithClient('client.brPop', async client => { - const [ brPopReply ] = await Promise.all([ - client.brPop( - commandOptions({ isolated: true }), - 'key', - 1 - ), - client.lPush('key', 'element'), - ]); - - assert.deepEqual( - brPopReply, - { - key: 'key', - element: 'element' - } - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.brPop', async cluster => { - const [ brPopReply ] = await Promise.all([ - cluster.brPop( - commandOptions({ isolated: true }), - 'key', - 1 - ), - cluster.lPush('key', 'element'), - ]); - - assert.deepEqual( - brPopReply, - { - key: 'key', - element: 'element' - } - ); - }, GLOBAL.CLUSTERS.OPEN); + }); + + testUtils.testAll('brPop - null', async client => { + assert.equal( + await client.brPop('key', BLOCKING_MIN_VALUE), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testAll('brPopblPop - with member', async client => { + const [, reply] = await Promise.all([ + client.lPush('key', 'element'), + client.brPop('key', 1) + ]); + + assert.deepEqual(reply, { + key: 'key', + element: 'element' + }); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BRPOP.ts b/packages/client/lib/commands/BRPOP.ts index b30e7e2cc29..f9c8aaa5037 100644 --- a/packages/client/lib/commands/BRPOP.ts +++ b/packages/client/lib/commands/BRPOP.ts @@ -1,17 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import BLPOP from './BLPOP'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument | Array, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisVariadicArgument, timeout: number -): RedisCommandArguments { - const args = pushVerdictArguments(['BRPOP'], key); - + ) { + const args = pushVariadicArguments(['BRPOP'], key); args.push(timeout.toString()); - return args; -} - -export { transformReply } from './BLPOP'; + }, + transformReply: BLPOP.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BRPOPLPUSH.spec.ts b/packages/client/lib/commands/BRPOPLPUSH.spec.ts index 214af4553ac..1f6dc48bfea 100644 --- a/packages/client/lib/commands/BRPOPLPUSH.spec.ts +++ b/packages/client/lib/commands/BRPOPLPUSH.spec.ts @@ -1,47 +1,42 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './BRPOPLPUSH'; -import { commandOptions } from '../../index'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; +import BRPOPLPUSH from './BRPOPLPUSH'; describe('BRPOPLPUSH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination', 0), - ['BRPOPLPUSH', 'source', 'destination', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + BRPOPLPUSH.transformArguments('source', 'destination', 0), + ['BRPOPLPUSH', 'source', 'destination', '0'] + ); + }); - testUtils.testWithClient('client.brPopLPush', async client => { - const [ popReply ] = await Promise.all([ - client.brPopLPush( - commandOptions({ isolated: true }), - 'source', - 'destination', - 0 - ), - client.lPush('source', 'element') - ]); + testUtils.testAll('brPopLPush - null', async client => { + assert.equal( + await client.brPopLPush( + '{tag}source', + '{tag}destination', + BLOCKING_MIN_VALUE + ), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); - assert.equal( - popReply, - 'element' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('brPopLPush - with member', async client => { + const [, reply] = await Promise.all([ + client.lPush('{tag}source', 'element'), + client.brPopLPush( + '{tag}source', + '{tag}destination', + 0 + ) + ]); - testUtils.testWithCluster('cluster.brPopLPush', async cluster => { - const [ popReply ] = await Promise.all([ - cluster.brPopLPush( - commandOptions({ isolated: true }), - '{tag}source', - '{tag}destination', - 0 - ), - cluster.lPush('{tag}source', 'element') - ]); - - assert.equal( - popReply, - 'element' - ); - }, GLOBAL.CLUSTERS.OPEN); + assert.equal(reply, 'element'); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BRPOPLPUSH.ts b/packages/client/lib/commands/BRPOPLPUSH.ts index 72c3e4aa5b2..d342ea75725 100644 --- a/packages/client/lib/commands/BRPOPLPUSH.ts +++ b/packages/client/lib/commands/BRPOPLPUSH.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + source: RedisArgument, + destination: RedisArgument, timeout: number -): RedisCommandArguments { + ) { return ['BRPOPLPUSH', source, destination, timeout.toString()]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BZMPOP.spec.ts b/packages/client/lib/commands/BZMPOP.spec.ts index 0e381c114f2..554e6898d62 100644 --- a/packages/client/lib/commands/BZMPOP.spec.ts +++ b/packages/client/lib/commands/BZMPOP.spec.ts @@ -1,32 +1,55 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './BZMPOP'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; +import BZMPOP from './BZMPOP'; describe('BZMPOP', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(0, 'key', 'MIN'), - ['BZMPOP', '0', '1', 'key', 'MIN'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + BZMPOP.transformArguments(0, 'key', 'MIN'), + ['BZMPOP', '0', '1', 'key', 'MIN'] + ); + }); - it('with COUNT', () => { - assert.deepEqual( - transformArguments(0, 'key', 'MIN', { - COUNT: 2 - }), - ['BZMPOP', '0', '1', 'key', 'MIN', 'COUNT', '2'] - ); - }); + it('with COUNT', () => { + assert.deepEqual( + BZMPOP.transformArguments(0, 'key', 'MIN', { + COUNT: 2 + }), + ['BZMPOP', '0', '1', 'key', 'MIN', 'COUNT', '2'] + ); }); + }); + + testUtils.testAll('bzmPop - null', async client => { + assert.equal( + await client.bzmPop(BLOCKING_MIN_VALUE, 'key', 'MAX'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); - testUtils.testWithClient('client.bzmPop', async client => { - assert.deepEqual( - await client.bzmPop(1, 'key', 'MAX'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('bzmPop - with member', async client => { + const key = 'key', + member = { + value: 'a', + score: 1 + }, + [, reply] = await Promise.all([ + client.zAdd(key, member), + client.bzmPop(BLOCKING_MIN_VALUE, key, 'MAX') + ]); + + assert.deepEqual(reply, { + key, + members: [member] + }); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BZMPOP.ts b/packages/client/lib/commands/BZMPOP.ts index e4e9699cbd4..030aa20c66b 100644 --- a/packages/client/lib/commands/BZMPOP.ts +++ b/packages/client/lib/commands/BZMPOP.ts @@ -1,20 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { SortedSetSide, transformZMPopArguments, ZMPopOptions } from './generic-transformers'; +import { Command } from '../RESP/types'; +import ZMPOP, { ZMPopArguments, transformZMPopArguments } from './ZMPOP'; -export const FIRST_KEY_INDEX = 3; - -export function transformArguments( - timeout: number, - keys: RedisCommandArgument | Array, - side: SortedSetSide, - options?: ZMPopOptions -): RedisCommandArguments { - return transformZMPopArguments( - ['BZMPOP', timeout.toString()], - keys, - side, - options - ); -} - -export { transformReply } from './ZMPOP'; +export default { + FIRST_KEY_INDEX: 3, + IS_READ_ONLY: false, + transformArguments(timeout: number, ...args: ZMPopArguments) { + return transformZMPopArguments(['BZMPOP', timeout.toString()], ...args); + }, + transformReply: ZMPOP.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/BZPOPMAX.spec.ts b/packages/client/lib/commands/BZPOPMAX.spec.ts index d5c17437122..1f0a4d44f07 100644 --- a/packages/client/lib/commands/BZPOPMAX.spec.ts +++ b/packages/client/lib/commands/BZPOPMAX.spec.ts @@ -1,65 +1,51 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './BZPOPMAX'; -import { commandOptions } from '../../index'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; +import BZPOPMAX from './BZPOPMAX'; 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('transformArguments', () => { + it('single', () => { + assert.deepEqual( + BZPOPMAX.transformArguments('key', 0), + ['BZPOPMAX', 'key', '0'] + ); }); - describe('transformReply', () => { - it('null', () => { - assert.equal( - transformReply(null), - null - ); - }); - - it('member', () => { - assert.deepEqual( - transformReply(['key', 'value', '1']), - { - key: 'key', - value: 'value', - score: 1 - } - ); - }); + it('multiple', () => { + assert.deepEqual( + BZPOPMAX.transformArguments(['1', '2'], 0), + ['BZPOPMAX', '1', '2', '0'] + ); }); + }); - testUtils.testWithClient('client.bzPopMax', async client => { - const [ bzPopMaxReply ] = await Promise.all([ - client.bzPopMax( - commandOptions({ isolated: true }), - 'key', - 1 - ), - client.zAdd('key', [{ - value: '1', - score: 1 - }]) - ]); + testUtils.testAll('bzPopMax - null', async client => { + assert.equal( + await client.bzPopMax('key', BLOCKING_MIN_VALUE), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); - assert.deepEqual( - bzPopMaxReply, - { - key: 'key', - value: '1', - score: 1 - } - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('bzPopMax - with member', async client => { + const key = 'key', + member = { + value: 'a', + score: 1 + }, + [, reply] = await Promise.all([ + client.zAdd(key, member), + client.bzPopMax(key, BLOCKING_MIN_VALUE) + ]); + + assert.deepEqual(reply, { + key, + ...member + }); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BZPOPMAX.ts b/packages/client/lib/commands/BZPOPMAX.ts index 94a30fb8dce..792a5592574 100644 --- a/packages/client/lib/commands/BZPOPMAX.ts +++ b/packages/client/lib/commands/BZPOPMAX.ts @@ -1,29 +1,43 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments, transformNumberInfinityReply, ZMember } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument | Array, - timeout: number -): RedisCommandArguments { - const args = pushVerdictArguments(['BZPOPMAX'], key); - - args.push(timeout.toString()); - - return args; +import { RedisArgument, NullReply, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments, transformDoubleReply } from './generic-transformers'; + +export function transformBZPopArguments( + command: RedisArgument, + key: RedisVariadicArgument, + timeout: number +) { + const args = pushVariadicArguments([command], key); + args.push(timeout.toString()); + return args; } -type ZMemberRawReply = [key: RedisCommandArgument, value: RedisCommandArgument, score: RedisCommandArgument] | null; - -type BZPopMaxReply = (ZMember & { key: RedisCommandArgument }) | null; - -export function transformReply(reply: ZMemberRawReply): BZPopMaxReply | null { - if (!reply) return null; - - return { +export type BZPopArguments = typeof transformBZPopArguments extends (_: any, ...args: infer T) => any ? T : never; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(...args: BZPopArguments) { + return transformBZPopArguments('BZPOPMAX', ...args); + }, + transformReply: { + 2( + reply: UnwrapReply>, + preserve?: any, + typeMapping?: TypeMapping + ) { + return reply === null ? null : { key: reply[0], value: reply[1], - score: transformNumberInfinityReply(reply[2]) - }; -} + score: transformDoubleReply[2](reply[2], preserve, typeMapping) + }; + }, + 3(reply: UnwrapReply>) { + return reply === null ? null : { + key: reply[0], + value: reply[1], + score: reply[2] + }; + } + } +} as const satisfies Command; + diff --git a/packages/client/lib/commands/BZPOPMIN.spec.ts b/packages/client/lib/commands/BZPOPMIN.spec.ts index 0573a4ac898..7f39f7d1896 100644 --- a/packages/client/lib/commands/BZPOPMIN.spec.ts +++ b/packages/client/lib/commands/BZPOPMIN.spec.ts @@ -1,65 +1,51 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './BZPOPMIN'; -import { commandOptions } from '../../index'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; +import BZPOPMIN from './BZPOPMIN'; 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('transformArguments', () => { + it('single', () => { + assert.deepEqual( + BZPOPMIN.transformArguments('key', 0), + ['BZPOPMIN', 'key', '0'] + ); }); - describe('transformReply', () => { - it('null', () => { - assert.equal( - transformReply(null), - null - ); - }); - - it('member', () => { - assert.deepEqual( - transformReply(['key', 'value', '1']), - { - key: 'key', - value: 'value', - score: 1 - } - ); - }); + it('multiple', () => { + assert.deepEqual( + BZPOPMIN.transformArguments(['1', '2'], 0), + ['BZPOPMIN', '1', '2', '0'] + ); }); + }); - testUtils.testWithClient('client.bzPopMin', async client => { - const [ bzPopMinReply ] = await Promise.all([ - client.bzPopMin( - commandOptions({ isolated: true }), - 'key', - 1 - ), - client.zAdd('key', [{ - value: '1', - score: 1 - }]) - ]); + testUtils.testAll('bzPopMin - null', async client => { + assert.equal( + await client.bzPopMin('key', BLOCKING_MIN_VALUE), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); - assert.deepEqual( - bzPopMinReply, - { - key: 'key', - value: '1', - score: 1 - } - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('bzPopMin - with member', async client => { + const key = 'key', + member = { + value: 'a', + score: 1 + }, + [, reply] = await Promise.all([ + client.zAdd(key, member), + client.bzPopMin(key, BLOCKING_MIN_VALUE) + ]); + + assert.deepEqual(reply, { + key, + ...member + }); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); }); diff --git a/packages/client/lib/commands/BZPOPMIN.ts b/packages/client/lib/commands/BZPOPMIN.ts index 40cb3d5dc75..f27e623528e 100644 --- a/packages/client/lib/commands/BZPOPMIN.ts +++ b/packages/client/lib/commands/BZPOPMIN.ts @@ -1,17 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import BZPOPMAX, { BZPopArguments, transformBZPopArguments } from './BZPOPMAX'; + +export default { + FIRST_KEY_INDEX: BZPOPMAX.FIRST_KEY_INDEX, + IS_READ_ONLY: BZPOPMAX.IS_READ_ONLY, + transformArguments(...args: BZPopArguments) { + return transformBZPopArguments('BZPOPMIN', ...args); + }, + transformReply: BZPOPMAX.transformReply +} as const satisfies Command; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument | Array, - timeout: number -): RedisCommandArguments { - const args = pushVerdictArguments(['BZPOPMIN'], key); - - args.push(timeout.toString()); - - return args; -} - -export { transformReply } from './BZPOPMAX'; diff --git a/packages/client/lib/commands/CLIENT_CACHING.spec.ts b/packages/client/lib/commands/CLIENT_CACHING.spec.ts index d9cb9a3f796..34023f98922 100644 --- a/packages/client/lib/commands/CLIENT_CACHING.spec.ts +++ b/packages/client/lib/commands/CLIENT_CACHING.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLIENT_CACHING'; +import { strict as assert } from 'node:assert'; +import CLIENT_CACHING from './CLIENT_CACHING'; describe('CLIENT CACHING', () => { - describe('transformArguments', () => { - it('true', () => { - assert.deepEqual( - transformArguments(true), - ['CLIENT', 'CACHING', 'YES'] - ); - }); + describe('transformArguments', () => { + it('true', () => { + assert.deepEqual( + CLIENT_CACHING.transformArguments(true), + ['CLIENT', 'CACHING', 'YES'] + ); + }); - it('false', () => { - assert.deepEqual( - transformArguments(false), - ['CLIENT', 'CACHING', 'NO'] - ); - }); + it('false', () => { + assert.deepEqual( + CLIENT_CACHING.transformArguments(false), + ['CLIENT', 'CACHING', 'NO'] + ); }); + }); }); diff --git a/packages/client/lib/commands/CLIENT_CACHING.ts b/packages/client/lib/commands/CLIENT_CACHING.ts index bc2fbe41e9d..505ae152f85 100644 --- a/packages/client/lib/commands/CLIENT_CACHING.ts +++ b/packages/client/lib/commands/CLIENT_CACHING.ts @@ -1,11 +1,14 @@ -import { RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(value: boolean): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(value: boolean) { return [ - 'CLIENT', - 'CACHING', - value ? 'YES' : 'NO' + 'CLIENT', + 'CACHING', + value ? 'YES' : 'NO' ]; -} - -export declare function transformReply(): 'OK' | Buffer; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_GETNAME.spec.ts b/packages/client/lib/commands/CLIENT_GETNAME.spec.ts index 0a09713882f..8975f1fee9c 100644 --- a/packages/client/lib/commands/CLIENT_GETNAME.spec.ts +++ b/packages/client/lib/commands/CLIENT_GETNAME.spec.ts @@ -1,11 +1,19 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLIENT_GETNAME'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import CLIENT_GETNAME from './CLIENT_GETNAME'; describe('CLIENT GETNAME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'GETNAME'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLIENT_GETNAME.transformArguments(), + ['CLIENT', 'GETNAME'] + ); + }); + + testUtils.testWithClient('client.clientGetName', async client => { + assert.equal( + await client.clientGetName(), + null + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_GETNAME.ts b/packages/client/lib/commands/CLIENT_GETNAME.ts index da00539d7fb..c46b576407b 100644 --- a/packages/client/lib/commands/CLIENT_GETNAME.ts +++ b/packages/client/lib/commands/CLIENT_GETNAME.ts @@ -1,7 +1,13 @@ -import { RedisCommandArguments } from '.'; +import { BlobStringReply, NullReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['CLIENT', 'GETNAME']; -} - -export declare function transformReply(): string | null; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return [ + 'CLIENT', + 'GETNAME' + ]; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts b/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts index 09dd9677e32..5cfedf2a4e7 100644 --- a/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts +++ b/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLIENT_GETREDIR'; +import { strict as assert } from 'node:assert'; +import CLIENT_GETREDIR from './CLIENT_GETREDIR'; describe('CLIENT GETREDIR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'GETREDIR'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLIENT_GETREDIR.transformArguments(), + ['CLIENT', 'GETREDIR'] + ); + }); }); diff --git a/packages/client/lib/commands/CLIENT_GETREDIR.ts b/packages/client/lib/commands/CLIENT_GETREDIR.ts index d192adf284a..ae0b601b4e8 100644 --- a/packages/client/lib/commands/CLIENT_GETREDIR.ts +++ b/packages/client/lib/commands/CLIENT_GETREDIR.ts @@ -1,7 +1,10 @@ -import { RedisCommandArguments } from '.'; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['CLIENT', 'GETREDIR']; -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLIENT', 'GETREDIR'] + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_ID.spec.ts b/packages/client/lib/commands/CLIENT_ID.spec.ts index 6792a8c31be..7b51e6bd930 100644 --- a/packages/client/lib/commands/CLIENT_ID.spec.ts +++ b/packages/client/lib/commands/CLIENT_ID.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLIENT_ID'; +import CLIENT_ID from './CLIENT_ID'; describe('CLIENT ID', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'ID'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLIENT_ID.transformArguments(), + ['CLIENT', 'ID'] + ); + }); - testUtils.testWithClient('client.clientId', async client => { - assert.equal( - typeof (await client.clientId()), - 'number' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.clientId', async client => { + assert.equal( + typeof (await client.clientId()), + 'number' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_ID.ts b/packages/client/lib/commands/CLIENT_ID.ts index a57e392ade6..165ab1931eb 100644 --- a/packages/client/lib/commands/CLIENT_ID.ts +++ b/packages/client/lib/commands/CLIENT_ID.ts @@ -1,7 +1,10 @@ -export const IS_READ_ONLY = true; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['CLIENT', 'ID']; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_INFO.spec.ts b/packages/client/lib/commands/CLIENT_INFO.spec.ts index ccb99017cf3..0aba384aa3a 100644 --- a/packages/client/lib/commands/CLIENT_INFO.spec.ts +++ b/packages/client/lib/commands/CLIENT_INFO.spec.ts @@ -1,50 +1,50 @@ -import { strict as assert } from 'assert'; -import { transformArguments, transformReply } from './CLIENT_INFO'; +import { strict as assert } from 'node:assert'; +import CLIENT_INFO from './CLIENT_INFO'; import testUtils, { GLOBAL } from '../test-utils'; describe('CLIENT INFO', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'INFO'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLIENT_INFO.transformArguments(), + ['CLIENT', 'INFO'] + ); + }); - testUtils.testWithClient('client.clientInfo', async client => { - const reply = await client.clientInfo(); - assert.equal(typeof reply.id, 'number'); - assert.equal(typeof reply.addr, 'string'); - assert.equal(typeof reply.laddr, 'string'); - assert.equal(typeof reply.fd, 'number'); - assert.equal(typeof reply.name, 'string'); - assert.equal(typeof reply.age, 'number'); - assert.equal(typeof reply.idle, 'number'); - assert.equal(typeof reply.flags, 'string'); - assert.equal(typeof reply.db, 'number'); - assert.equal(typeof reply.sub, 'number'); - assert.equal(typeof reply.psub, 'number'); - assert.equal(typeof reply.multi, 'number'); - assert.equal(typeof reply.qbuf, 'number'); - assert.equal(typeof reply.qbufFree, 'number'); - assert.equal(typeof reply.argvMem, 'number'); - assert.equal(typeof reply.obl, 'number'); - assert.equal(typeof reply.oll, 'number'); - assert.equal(typeof reply.omem, 'number'); - assert.equal(typeof reply.totMem, 'number'); - assert.equal(typeof reply.events, 'string'); - assert.equal(typeof reply.cmd, 'string'); - assert.equal(typeof reply.user, 'string'); - assert.equal(typeof reply.redir, 'number'); + testUtils.testWithClient('client.clientInfo', async client => { + const reply = await client.clientInfo(); + assert.equal(typeof reply.id, 'number'); + assert.equal(typeof reply.addr, 'string'); + assert.equal(typeof reply.laddr, 'string'); + assert.equal(typeof reply.fd, 'number'); + assert.equal(typeof reply.name, 'string'); + assert.equal(typeof reply.age, 'number'); + assert.equal(typeof reply.idle, 'number'); + assert.equal(typeof reply.flags, 'string'); + assert.equal(typeof reply.db, 'number'); + assert.equal(typeof reply.sub, 'number'); + assert.equal(typeof reply.psub, 'number'); + assert.equal(typeof reply.multi, 'number'); + assert.equal(typeof reply.qbuf, 'number'); + assert.equal(typeof reply.qbufFree, 'number'); + assert.equal(typeof reply.argvMem, 'number'); + assert.equal(typeof reply.obl, 'number'); + assert.equal(typeof reply.oll, 'number'); + assert.equal(typeof reply.omem, 'number'); + assert.equal(typeof reply.totMem, 'number'); + assert.equal(typeof reply.events, 'string'); + assert.equal(typeof reply.cmd, 'string'); + assert.equal(typeof reply.user, 'string'); + assert.equal(typeof reply.redir, 'number'); - if (testUtils.isVersionGreaterThan([7, 0])) { - assert.equal(typeof reply.multiMem, 'number'); - assert.equal(typeof reply.resp, 'number'); - } + if (testUtils.isVersionGreaterThan([7, 0])) { + assert.equal(typeof reply.multiMem, 'number'); + assert.equal(typeof reply.resp, 'number'); - if (testUtils.isVersionGreaterThan([7, 0, 3])) { - assert.equal(typeof reply.ssub, 'number'); - } - }, GLOBAL.SERVERS.OPEN); + if (testUtils.isVersionGreaterThan([7, 0, 3])) { + assert.equal(typeof reply.ssub, 'number'); + } + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_INFO.ts b/packages/client/lib/commands/CLIENT_INFO.ts index fd823542f86..88721e2f8b9 100644 --- a/packages/client/lib/commands/CLIENT_INFO.ts +++ b/packages/client/lib/commands/CLIENT_INFO.ts @@ -1,94 +1,116 @@ -export const IS_READ_ONLY = true; - -export function transformArguments(): Array { - return ['CLIENT', 'INFO']; -} +import { Command, VerbatimStringReply } from '../RESP/types'; export interface ClientInfoReply { - id: number; - addr: string; - laddr?: string; // 6.2 - fd: number; - name: string; - age: number; - idle: number; - flags: string; - db: number; - sub: number; - psub: number; - ssub?: number; // 7.0.3 - multi: number; - qbuf: number; - qbufFree: number; - argvMem?: number; // 6.0 - multiMem?: number; // 7.0 - obl: number; - oll: number; - omem: number; - totMem?: number; // 6.0 - events: string; - cmd: string; - user?: string; // 6.0 - redir?: number; // 6.2 - resp?: number; // 7.0 - // 7.2 - libName?: string; - libVer?: string; + id: number; + addr: string; + /** + * available since 6.2 + */ + laddr?: string; + fd: number; + name: string; + age: number; + idle: number; + flags: string; + db: number; + sub: number; + psub: number; + /** + * available since 7.0.3 + */ + ssub?: number; + multi: number; + qbuf: number; + qbufFree: number; + /** + * available since 6.0 + */ + argvMem?: number; + /** + * available since 7.0 + */ + multiMem?: number; + obl: number; + oll: number; + omem: number; + /** + * available since 6.0 + */ + totMem?: number; + events: string; + cmd: string; + /** + * available since 6.0 + */ + user?: string; + /** + * available since 6.2 + */ + redir?: number; + /** + * available since 7.0 + */ + resp?: number; } const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g; -export function transformReply(rawReply: string): ClientInfoReply { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLIENT', 'INFO'] + }, + transformReply(rawReply: VerbatimStringReply) { const map: Record = {}; - for (const item of rawReply.matchAll(CLIENT_INFO_REGEX)) { - map[item[1]] = item[2]; + for (const item of rawReply.toString().matchAll(CLIENT_INFO_REGEX)) { + map[item[1]] = item[2]; } const reply: ClientInfoReply = { - id: Number(map.id), - addr: map.addr, - fd: Number(map.fd), - name: map.name, - age: Number(map.age), - idle: Number(map.idle), - flags: map.flags, - db: Number(map.db), - sub: Number(map.sub), - psub: Number(map.psub), - multi: Number(map.multi), - qbuf: Number(map.qbuf), - qbufFree: Number(map['qbuf-free']), - argvMem: Number(map['argv-mem']), - obl: Number(map.obl), - oll: Number(map.oll), - omem: Number(map.omem), - totMem: Number(map['tot-mem']), - events: map.events, - cmd: map.cmd, - user: map.user, - libName: map['lib-name'], - libVer: map['lib-ver'], + id: Number(map.id), + addr: map.addr, + fd: Number(map.fd), + name: map.name, + age: Number(map.age), + idle: Number(map.idle), + flags: map.flags, + db: Number(map.db), + sub: Number(map.sub), + psub: Number(map.psub), + multi: Number(map.multi), + qbuf: Number(map.qbuf), + qbufFree: Number(map['qbuf-free']), + argvMem: Number(map['argv-mem']), + obl: Number(map.obl), + oll: Number(map.oll), + omem: Number(map.omem), + totMem: Number(map['tot-mem']), + events: map.events, + cmd: map.cmd, + user: map.user }; if (map.laddr !== undefined) { - reply.laddr = map.laddr; + reply.laddr = map.laddr; } if (map.redir !== undefined) { - reply.redir = Number(map.redir); + reply.redir = Number(map.redir); } if (map.ssub !== undefined) { - reply.ssub = Number(map.ssub); + reply.ssub = Number(map.ssub); } if (map['multi-mem'] !== undefined) { - reply.multiMem = Number(map['multi-mem']); + reply.multiMem = Number(map['multi-mem']); } if (map.resp !== undefined) { - reply.resp = Number(map.resp); + reply.resp = Number(map.resp); } return reply; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_KILL.spec.ts b/packages/client/lib/commands/CLIENT_KILL.spec.ts index 733aaca858b..79254af41f9 100644 --- a/packages/client/lib/commands/CLIENT_KILL.spec.ts +++ b/packages/client/lib/commands/CLIENT_KILL.spec.ts @@ -1,120 +1,120 @@ -import { strict as assert } from 'assert'; -import { ClientKillFilters, transformArguments } from './CLIENT_KILL'; +import { strict as assert } from 'node:assert'; +import CLIENT_KILL, { CLIENT_KILL_FILTERS } from './CLIENT_KILL'; describe('CLIENT KILL', () => { - describe('transformArguments', () => { - it('ADDRESS', () => { - assert.deepEqual( - transformArguments({ - filter: ClientKillFilters.ADDRESS, - address: 'ip:6379' - }), - ['CLIENT', 'KILL', 'ADDR', 'ip:6379'] - ); - }); + describe('transformArguments', () => { + it('ADDRESS', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments({ + filter: CLIENT_KILL_FILTERS.ADDRESS, + address: 'ip:6379' + }), + ['CLIENT', 'KILL', 'ADDR', 'ip:6379'] + ); + }); + + it('LOCAL_ADDRESS', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments({ + filter: CLIENT_KILL_FILTERS.LOCAL_ADDRESS, + localAddress: 'ip:6379' + }), + ['CLIENT', 'KILL', 'LADDR', 'ip:6379'] + ); + }); - it('LOCAL_ADDRESS', () => { - assert.deepEqual( - transformArguments({ - filter: ClientKillFilters.LOCAL_ADDRESS, - localAddress: 'ip:6379' - }), - ['CLIENT', 'KILL', 'LADDR', 'ip:6379'] - ); - }); + describe('ID', () => { + it('string', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments({ + filter: CLIENT_KILL_FILTERS.ID, + id: '1' + }), + ['CLIENT', 'KILL', 'ID', '1'] + ); + }); - describe('ID', () => { - it('string', () => { - assert.deepEqual( - transformArguments({ - filter: ClientKillFilters.ID, - id: '1' - }), - ['CLIENT', 'KILL', 'ID', '1'] - ); - }); + it('number', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments({ + filter: CLIENT_KILL_FILTERS.ID, + id: 1 + }), + ['CLIENT', 'KILL', 'ID', '1'] + ); + }); + }); - it('number', () => { - assert.deepEqual( - transformArguments({ - filter: ClientKillFilters.ID, - id: 1 - }), - ['CLIENT', 'KILL', 'ID', '1'] - ); - }); - }); + it('TYPE', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments({ + filter: CLIENT_KILL_FILTERS.TYPE, + type: 'master' + }), + ['CLIENT', 'KILL', 'TYPE', 'master'] + ); + }); - it('TYPE', () => { - assert.deepEqual( - transformArguments({ - filter: ClientKillFilters.TYPE, - type: 'master' - }), - ['CLIENT', 'KILL', 'TYPE', 'master'] - ); - }); + it('USER', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments({ + filter: CLIENT_KILL_FILTERS.USER, + username: 'username' + }), + ['CLIENT', 'KILL', 'USER', 'username'] + ); + }); - it('USER', () => { - assert.deepEqual( - transformArguments({ - filter: ClientKillFilters.USER, - username: 'username' - }), - ['CLIENT', 'KILL', 'USER', 'username'] - ); - }); + it('MAXAGE', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments({ + filter: CLIENT_KILL_FILTERS.MAXAGE, + maxAge: 10 + }), + ['CLIENT', 'KILL', 'MAXAGE', '10'] + ); + }); - it('MAXAGE', () => { - assert.deepEqual( - transformArguments({ - filter: ClientKillFilters.MAXAGE, - maxAge: 10 - }), - ['CLIENT', 'KILL', 'MAXAGE', '10'] - ); - }); + describe('SKIP_ME', () => { + it('undefined', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments(CLIENT_KILL_FILTERS.SKIP_ME), + ['CLIENT', 'KILL', 'SKIPME'] + ); + }); - describe('SKIP_ME', () => { - it('undefined', () => { - assert.deepEqual( - transformArguments(ClientKillFilters.SKIP_ME), - ['CLIENT', 'KILL', 'SKIPME'] - ); - }); + it('true', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments({ + filter: CLIENT_KILL_FILTERS.SKIP_ME, + skipMe: true + }), + ['CLIENT', 'KILL', 'SKIPME', 'yes'] + ); + }); - it('true', () => { - assert.deepEqual( - transformArguments({ - filter: ClientKillFilters.SKIP_ME, - skipMe: true - }), - ['CLIENT', 'KILL', 'SKIPME', 'yes'] - ); - }); + it('false', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments({ + filter: CLIENT_KILL_FILTERS.SKIP_ME, + skipMe: false + }), + ['CLIENT', 'KILL', 'SKIPME', 'no'] + ); + }); + }); - it('false', () => { - assert.deepEqual( - transformArguments({ - filter: ClientKillFilters.SKIP_ME, - skipMe: false - }), - ['CLIENT', 'KILL', 'SKIPME', 'no'] - ); - }); - }); - - it('TYPE & SKIP_ME', () => { - assert.deepEqual( - transformArguments([ - { - filter: ClientKillFilters.TYPE, - type: 'master' - }, - ClientKillFilters.SKIP_ME - ]), - ['CLIENT', 'KILL', 'TYPE', 'master', 'SKIPME'] - ); - }); + it('TYPE & SKIP_ME', () => { + assert.deepEqual( + CLIENT_KILL.transformArguments([ + { + filter: CLIENT_KILL_FILTERS.TYPE, + type: 'master' + }, + CLIENT_KILL_FILTERS.SKIP_ME + ]), + ['CLIENT', 'KILL', 'TYPE', 'master', 'SKIPME'] + ); }); + }); }); diff --git a/packages/client/lib/commands/CLIENT_KILL.ts b/packages/client/lib/commands/CLIENT_KILL.ts index b1a53df64d8..c5eb5304c57 100644 --- a/packages/client/lib/commands/CLIENT_KILL.ts +++ b/packages/client/lib/commands/CLIENT_KILL.ts @@ -1,104 +1,109 @@ -import { RedisCommandArguments } from '.'; - -export enum ClientKillFilters { - ADDRESS = 'ADDR', - LOCAL_ADDRESS = 'LADDR', - ID = 'ID', - TYPE = 'TYPE', - USER = 'USER', - SKIP_ME = 'SKIPME', - MAXAGE = 'MAXAGE' +import { RedisArgument, NumberReply, Command } from '../RESP/types'; + +export const CLIENT_KILL_FILTERS = { + ADDRESS: 'ADDR', + LOCAL_ADDRESS: 'LADDR', + ID: 'ID', + TYPE: 'TYPE', + USER: 'USER', + SKIP_ME: 'SKIPME', + MAXAGE: 'MAXAGE' +} as const; + +type CLIENT_KILL_FILTERS = typeof CLIENT_KILL_FILTERS; + +export interface ClientKillFilterCommon { + filter: T; } -interface KillFilter { - filter: T; +export interface ClientKillAddress extends ClientKillFilterCommon { + address: `${string}:${number}`; } -interface KillAddress extends KillFilter { - address: `${string}:${number}`; +export interface ClientKillLocalAddress extends ClientKillFilterCommon { + localAddress: `${string}:${number}`; } -interface KillLocalAddress extends KillFilter { - localAddress: `${string}:${number}`; +export interface ClientKillId extends ClientKillFilterCommon { + id: number | `${number}`; } -interface KillId extends KillFilter { - id: number | `${number}`; +export interface ClientKillType extends ClientKillFilterCommon { + type: 'normal' | 'master' | 'replica' | 'pubsub'; } -interface KillType extends KillFilter { - type: 'normal' | 'master' | 'replica' | 'pubsub'; +export interface ClientKillUser extends ClientKillFilterCommon { + username: string; } -interface KillUser extends KillFilter { - username: string; -} - -type KillSkipMe = ClientKillFilters.SKIP_ME | (KillFilter & { - skipMe: boolean; +export type ClientKillSkipMe = CLIENT_KILL_FILTERS['SKIP_ME'] | (ClientKillFilterCommon & { + skipMe: boolean; }); -interface KillMaxAge extends KillFilter { - maxAge: number; +export interface ClientKillMaxAge extends ClientKillFilterCommon { + maxAge: number; } -type KillFilters = KillAddress | KillLocalAddress | KillId | KillType | KillUser | KillSkipMe | KillMaxAge; +export type ClientKillFilter = ClientKillAddress | ClientKillLocalAddress | ClientKillId | ClientKillType | ClientKillUser | ClientKillSkipMe | ClientKillMaxAge; -export function transformArguments(filters: KillFilters | Array): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(filters: ClientKillFilter | Array) { const args = ['CLIENT', 'KILL']; if (Array.isArray(filters)) { - for (const filter of filters) { - pushFilter(args, filter); - } + for (const filter of filters) { + pushFilter(args, filter); + } } else { - pushFilter(args, filters); + pushFilter(args, filters); } return args; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; + +function pushFilter(args: Array, filter: ClientKillFilter): void { + if (filter === CLIENT_KILL_FILTERS.SKIP_ME) { + args.push('SKIPME'); + return; + } + + args.push(filter.filter); + + switch (filter.filter) { + case CLIENT_KILL_FILTERS.ADDRESS: + args.push(filter.address); + break; + + case CLIENT_KILL_FILTERS.LOCAL_ADDRESS: + args.push(filter.localAddress); + break; + + case CLIENT_KILL_FILTERS.ID: + args.push( + typeof filter.id === 'number' ? + filter.id.toString() : + filter.id + ); + break; + + case CLIENT_KILL_FILTERS.TYPE: + args.push(filter.type); + break; + + case CLIENT_KILL_FILTERS.USER: + args.push(filter.username); + break; + + case CLIENT_KILL_FILTERS.SKIP_ME: + args.push(filter.skipMe ? 'yes' : 'no'); + break; + + case CLIENT_KILL_FILTERS.MAXAGE: + args.push(filter.maxAge.toString()); + break; + } } - -function pushFilter(args: RedisCommandArguments, filter: KillFilters): void { - if (filter === ClientKillFilters.SKIP_ME) { - args.push('SKIPME'); - return; - } - - args.push(filter.filter); - - switch(filter.filter) { - case ClientKillFilters.ADDRESS: - args.push(filter.address); - break; - - case ClientKillFilters.LOCAL_ADDRESS: - args.push(filter.localAddress); - break; - - case ClientKillFilters.ID: - args.push( - typeof filter.id === 'number' ? - filter.id.toString() : - filter.id - ); - break; - - case ClientKillFilters.TYPE: - args.push(filter.type); - break; - - case ClientKillFilters.USER: - args.push(filter.username); - break; - - case ClientKillFilters.SKIP_ME: - args.push(filter.skipMe ? 'yes' : 'no'); - break; - - case ClientKillFilters.MAXAGE: - args.push(filter.maxAge.toString()); - break; - } -} - -export declare function transformReply(): number; diff --git a/packages/client/lib/commands/CLIENT_LIST.spec.ts b/packages/client/lib/commands/CLIENT_LIST.spec.ts index c9c720e12ef..e967a8dc0ff 100644 --- a/packages/client/lib/commands/CLIENT_LIST.spec.ts +++ b/packages/client/lib/commands/CLIENT_LIST.spec.ts @@ -1,78 +1,77 @@ -import { strict as assert } from 'assert'; -import { transformArguments, transformReply } from './CLIENT_LIST'; +import { strict as assert } from 'node:assert'; +import CLIENT_LIST from './CLIENT_LIST'; import testUtils, { GLOBAL } from '../test-utils'; describe('CLIENT LIST', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'LIST'] - ); - }); - - it('with TYPE', () => { - assert.deepEqual( - transformArguments({ - TYPE: 'NORMAL' - }), - ['CLIENT', 'LIST', 'TYPE', 'NORMAL'] - ); - }); - - it('with ID', () => { - assert.deepEqual( - transformArguments({ - ID: ['1', '2'] - }), - ['CLIENT', 'LIST', 'ID', '1', '2'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + CLIENT_LIST.transformArguments(), + ['CLIENT', 'LIST'] + ); }); - testUtils.testWithClient('client.clientList', async client => { - const reply = await client.clientList(); - assert.ok(Array.isArray(reply)); - - for (const item of reply) { - assert.equal(typeof item.id, 'number'); - assert.equal(typeof item.addr, 'string'); - assert.equal(typeof item.fd, 'number'); - assert.equal(typeof item.name, 'string'); - assert.equal(typeof item.age, 'number'); - assert.equal(typeof item.idle, 'number'); - assert.equal(typeof item.flags, 'string'); - assert.equal(typeof item.db, 'number'); - assert.equal(typeof item.sub, 'number'); - assert.equal(typeof item.psub, 'number'); - assert.equal(typeof item.multi, 'number'); - assert.equal(typeof item.qbuf, 'number'); - assert.equal(typeof item.qbufFree, 'number'); - assert.equal(typeof item.obl, 'number'); - assert.equal(typeof item.oll, 'number'); - assert.equal(typeof item.omem, 'number'); - assert.equal(typeof item.events, 'string'); - assert.equal(typeof item.cmd, 'string'); + it('with TYPE', () => { + assert.deepEqual( + CLIENT_LIST.transformArguments({ + TYPE: 'NORMAL' + }), + ['CLIENT', 'LIST', 'TYPE', 'NORMAL'] + ); + }); - if (testUtils.isVersionGreaterThan([6, 0])) { - assert.equal(typeof item.argvMem, 'number'); - assert.equal(typeof item.totMem, 'number'); - assert.equal(typeof item.user, 'string'); - } + it('with ID', () => { + assert.deepEqual( + CLIENT_LIST.transformArguments({ + ID: ['1', '2'] + }), + ['CLIENT', 'LIST', 'ID', '1', '2'] + ); + }); + }); - if (testUtils.isVersionGreaterThan([6, 2])) { - assert.equal(typeof item.redir, 'number'); - assert.equal(typeof item.laddr, 'string'); - } + testUtils.testWithClient('client.clientList', async client => { + const reply = await client.clientList(); + assert.ok(Array.isArray(reply)); + for (const item of reply) { + assert.equal(typeof item.id, 'number'); + assert.equal(typeof item.addr, 'string'); + assert.equal(typeof item.fd, 'number'); + assert.equal(typeof item.name, 'string'); + assert.equal(typeof item.age, 'number'); + assert.equal(typeof item.idle, 'number'); + assert.equal(typeof item.flags, 'string'); + assert.equal(typeof item.db, 'number'); + assert.equal(typeof item.sub, 'number'); + assert.equal(typeof item.psub, 'number'); + assert.equal(typeof item.multi, 'number'); + assert.equal(typeof item.qbuf, 'number'); + assert.equal(typeof item.qbufFree, 'number'); + assert.equal(typeof item.obl, 'number'); + assert.equal(typeof item.oll, 'number'); + assert.equal(typeof item.omem, 'number'); + assert.equal(typeof item.events, 'string'); + assert.equal(typeof item.cmd, 'string'); - if (testUtils.isVersionGreaterThan([7, 0])) { - assert.equal(typeof item.multiMem, 'number'); - assert.equal(typeof item.resp, 'number'); - } + if (testUtils.isVersionGreaterThan([6, 0])) { + assert.equal(typeof item.argvMem, 'number'); + assert.equal(typeof item.totMem, 'number'); + assert.equal(typeof item.user, 'string'); + + if (testUtils.isVersionGreaterThan([6, 2])) { + assert.equal(typeof item.redir, 'number'); + assert.equal(typeof item.laddr, 'string'); + + if (testUtils.isVersionGreaterThan([7, 0])) { + assert.equal(typeof item.multiMem, 'number'); + assert.equal(typeof item.resp, 'number'); if (testUtils.isVersionGreaterThan([7, 0, 3])) { - assert.equal(typeof item.ssub, 'number'); + assert.equal(typeof item.ssub, 'number'); } + } } - }, GLOBAL.SERVERS.OPEN); + } + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_LIST.ts b/packages/client/lib/commands/CLIENT_LIST.ts index 6f71dc7d999..dc43fb8855d 100644 --- a/packages/client/lib/commands/CLIENT_LIST.ts +++ b/packages/client/lib/commands/CLIENT_LIST.ts @@ -1,43 +1,44 @@ -import { RedisCommandArguments, RedisCommandArgument } from '.'; -import { pushVerdictArguments } from './generic-transformers'; -import { transformReply as transformClientInfoReply, ClientInfoReply } from './CLIENT_INFO'; +import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types'; +import { pushVariadicArguments } from './generic-transformers'; +import CLIENT_INFO, { ClientInfoReply } from './CLIENT_INFO'; -interface ListFilterType { - TYPE: 'NORMAL' | 'MASTER' | 'REPLICA' | 'PUBSUB'; - ID?: never; +export interface ListFilterType { + TYPE: 'NORMAL' | 'MASTER' | 'REPLICA' | 'PUBSUB'; + ID?: never; } -interface ListFilterId { - ID: Array; - TYPE?: never; +export interface ListFilterId { + ID: Array; + TYPE?: never; } export type ListFilter = ListFilterType | ListFilterId; -export const IS_READ_ONLY = true; - -export function transformArguments(filter?: ListFilter): RedisCommandArguments { - let args: RedisCommandArguments = ['CLIENT', 'LIST']; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(filter?: ListFilter) { + let args: Array = ['CLIENT', 'LIST']; if (filter) { - if (filter.TYPE !== undefined) { - args.push('TYPE', filter.TYPE); - } else { - args.push('ID'); - args = pushVerdictArguments(args, filter.ID); - } + if (filter.TYPE !== undefined) { + args.push('TYPE', filter.TYPE); + } else { + args.push('ID'); + args = pushVariadicArguments(args, filter.ID); + } } return args; -} - -export function transformReply(rawReply: string): Array { - const split = rawReply.split('\n'), - length = split.length - 1, - reply: Array = []; + }, + transformReply(rawReply: VerbatimStringReply): Array { + const split = rawReply.toString().split('\n'), + length = split.length - 1, + reply: Array = []; for (let i = 0; i < length; i++) { - reply.push(transformClientInfoReply(split[i])); + reply.push(CLIENT_INFO.transformReply(split[i] as unknown as VerbatimStringReply)); } - + return reply; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts b/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts index df8903f0646..5de4dfd7604 100644 --- a/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts +++ b/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts @@ -1,30 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLIENT_NO-EVICT'; +import CLIENT_NO_EVICT from './CLIENT_NO-EVICT'; describe('CLIENT NO-EVICT', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('true', () => { - assert.deepEqual( - transformArguments(true), - ['CLIENT', 'NO-EVICT', 'ON'] - ); - }); + describe('transformArguments', () => { + it('true', () => { + assert.deepEqual( + CLIENT_NO_EVICT.transformArguments(true), + ['CLIENT', 'NO-EVICT', 'ON'] + ); + }); - it('false', () => { - assert.deepEqual( - transformArguments(false), - ['CLIENT', 'NO-EVICT', 'OFF'] - ); - }); + it('false', () => { + assert.deepEqual( + CLIENT_NO_EVICT.transformArguments(false), + ['CLIENT', 'NO-EVICT', 'OFF'] + ); }); + }); - testUtils.testWithClient('client.clientNoEvict', async client => { - assert.equal( - await client.clientNoEvict(true), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.clientNoEvict', async client => { + assert.equal( + await client.clientNoEvict(true), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_NO-EVICT.ts b/packages/client/lib/commands/CLIENT_NO-EVICT.ts index 86edbde1d23..82aa50074ba 100644 --- a/packages/client/lib/commands/CLIENT_NO-EVICT.ts +++ b/packages/client/lib/commands/CLIENT_NO-EVICT.ts @@ -1,11 +1,14 @@ -import { RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(value: boolean): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(value: boolean) { return [ - 'CLIENT', - 'NO-EVICT', - value ? 'ON' : 'OFF' + 'CLIENT', + 'NO-EVICT', + value ? 'ON' : 'OFF' ]; -} - -export declare function transformReply(): 'OK' | Buffer; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts b/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts index 80ee0ada1fd..e58c22d9c6e 100644 --- a/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts +++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts @@ -1,30 +1,30 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLIENT_NO-TOUCH'; +import CLIENT_NO_TOUCH from './CLIENT_NO-TOUCH'; describe('CLIENT NO-TOUCH', () => { - testUtils.isVersionGreaterThanHook([7, 2]); + testUtils.isVersionGreaterThanHook([7, 2]); - describe('transformArguments', () => { - it('true', () => { - assert.deepEqual( - transformArguments(true), - ['CLIENT', 'NO-TOUCH', 'ON'] - ); - }); + describe('transformArguments', () => { + it('true', () => { + assert.deepEqual( + CLIENT_NO_TOUCH.transformArguments(true), + ['CLIENT', 'NO-TOUCH', 'ON'] + ); + }); - it('false', () => { - assert.deepEqual( - transformArguments(false), - ['CLIENT', 'NO-TOUCH', 'OFF'] - ); - }); + it('false', () => { + assert.deepEqual( + CLIENT_NO_TOUCH.transformArguments(false), + ['CLIENT', 'NO-TOUCH', 'OFF'] + ); }); + }); - testUtils.testWithClient('client.clientNoTouch', async client => { - assert.equal( - await client.clientNoTouch(true), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.clientNoTouch', async client => { + assert.equal( + await client.clientNoTouch(true), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts index d11f693dbab..a6fc5eb1765 100644 --- a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts +++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts @@ -1,11 +1,15 @@ -import { RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(value: boolean): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(value: boolean) { return [ - 'CLIENT', - 'NO-TOUCH', - value ? 'ON' : 'OFF' + 'CLIENT', + 'NO-TOUCH', + value ? 'ON' : 'OFF' ]; -} + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; -export declare function transformReply(): 'OK' | Buffer; diff --git a/packages/client/lib/commands/CLIENT_PAUSE.spec.ts b/packages/client/lib/commands/CLIENT_PAUSE.spec.ts index 1376ff41eed..a30f9075072 100644 --- a/packages/client/lib/commands/CLIENT_PAUSE.spec.ts +++ b/packages/client/lib/commands/CLIENT_PAUSE.spec.ts @@ -1,28 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLIENT_PAUSE'; +import CLIENT_PAUSE from './CLIENT_PAUSE'; describe('CLIENT PAUSE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(0), - ['CLIENT', 'PAUSE', '0'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + CLIENT_PAUSE.transformArguments(0), + ['CLIENT', 'PAUSE', '0'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments(0, 'ALL'), - ['CLIENT', 'PAUSE', '0', 'ALL'] - ); - }); + it('with mode', () => { + assert.deepEqual( + CLIENT_PAUSE.transformArguments(0, 'ALL'), + ['CLIENT', 'PAUSE', '0', 'ALL'] + ); }); + }); - testUtils.testWithClient('client.clientPause', async client => { - assert.equal( - await client.clientPause(0), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.clientPause', async client => { + assert.equal( + await client.clientPause(0), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_PAUSE.ts b/packages/client/lib/commands/CLIENT_PAUSE.ts index 090002272c9..87b4177ed8c 100644 --- a/packages/client/lib/commands/CLIENT_PAUSE.ts +++ b/packages/client/lib/commands/CLIENT_PAUSE.ts @@ -1,20 +1,20 @@ -import { RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments( - timeout: number, - mode?: 'WRITE' | 'ALL' -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(timeout: number, mode?: 'WRITE' | 'ALL') { const args = [ - 'CLIENT', - 'PAUSE', - timeout.toString() + 'CLIENT', + 'PAUSE', + timeout.toString() ]; if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export declare function transformReply(): 'OK' | Buffer; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_SETNAME.spec.ts b/packages/client/lib/commands/CLIENT_SETNAME.spec.ts index 96618f3f79f..8e6b914791d 100644 --- a/packages/client/lib/commands/CLIENT_SETNAME.spec.ts +++ b/packages/client/lib/commands/CLIENT_SETNAME.spec.ts @@ -1,11 +1,20 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLIENT_SETNAME'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; + +import CLIENT_SETNAME from './CLIENT_SETNAME'; describe('CLIENT SETNAME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('name'), - ['CLIENT', 'SETNAME', 'name'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLIENT_SETNAME.transformArguments('name'), + ['CLIENT', 'SETNAME', 'name'] + ); + }); + + testUtils.testWithClient('client.clientSetName', async client => { + assert.equal( + await client.clientSetName('name'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_SETNAME.ts b/packages/client/lib/commands/CLIENT_SETNAME.ts index f5cf1c786fb..e2e2a921958 100644 --- a/packages/client/lib/commands/CLIENT_SETNAME.ts +++ b/packages/client/lib/commands/CLIENT_SETNAME.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(name: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(name: RedisArgument) { return ['CLIENT', 'SETNAME', name]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_TRACKING.spec.ts b/packages/client/lib/commands/CLIENT_TRACKING.spec.ts index bbd0b13e777..98fe091fb1b 100644 --- a/packages/client/lib/commands/CLIENT_TRACKING.spec.ts +++ b/packages/client/lib/commands/CLIENT_TRACKING.spec.ts @@ -1,101 +1,101 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLIENT_TRACKING'; +import CLIENT_TRACKING from './CLIENT_TRACKING'; describe('CLIENT TRACKING', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - describe('transformArguments', () => { - describe('true', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(true), - ['CLIENT', 'TRACKING', 'ON'] - ); - }); + describe('transformArguments', () => { + describe('true', () => { + it('simple', () => { + assert.deepEqual( + CLIENT_TRACKING.transformArguments(true), + ['CLIENT', 'TRACKING', 'ON'] + ); + }); - it('with REDIRECT', () => { - assert.deepEqual( - transformArguments(true, { - REDIRECT: 1 - }), - ['CLIENT', 'TRACKING', 'ON', 'REDIRECT', '1'] - ); - }); + it('with REDIRECT', () => { + assert.deepEqual( + CLIENT_TRACKING.transformArguments(true, { + REDIRECT: 1 + }), + ['CLIENT', 'TRACKING', 'ON', 'REDIRECT', '1'] + ); + }); - describe('with BCAST', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(true, { - BCAST: true - }), - ['CLIENT', 'TRACKING', 'ON', 'BCAST'] - ); - }); + describe('with BCAST', () => { + it('simple', () => { + assert.deepEqual( + CLIENT_TRACKING.transformArguments(true, { + BCAST: true + }), + ['CLIENT', 'TRACKING', 'ON', 'BCAST'] + ); + }); - describe('with PREFIX', () => { - it('string', () => { - assert.deepEqual( - transformArguments(true, { - BCAST: true, - PREFIX: 'prefix' - }), - ['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', 'prefix'] - ); - }); + describe('with PREFIX', () => { + it('string', () => { + assert.deepEqual( + CLIENT_TRACKING.transformArguments(true, { + BCAST: true, + PREFIX: 'prefix' + }), + ['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', 'prefix'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(true, { - BCAST: true, - PREFIX: ['1', '2'] - }), - ['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', '1', 'PREFIX', '2'] - ); - }); - }); - }); + it('array', () => { + assert.deepEqual( + CLIENT_TRACKING.transformArguments(true, { + BCAST: true, + PREFIX: ['1', '2'] + }), + ['CLIENT', 'TRACKING', 'ON', 'BCAST', 'PREFIX', '1', 'PREFIX', '2'] + ); + }); + }); + }); - it('with OPTIN', () => { - assert.deepEqual( - transformArguments(true, { - OPTIN: true - }), - ['CLIENT', 'TRACKING', 'ON', 'OPTIN'] - ); - }); + it('with OPTIN', () => { + assert.deepEqual( + CLIENT_TRACKING.transformArguments(true, { + OPTIN: true + }), + ['CLIENT', 'TRACKING', 'ON', 'OPTIN'] + ); + }); - it('with OPTOUT', () => { - assert.deepEqual( - transformArguments(true, { - OPTOUT: true - }), - ['CLIENT', 'TRACKING', 'ON', 'OPTOUT'] - ); - }); + it('with OPTOUT', () => { + assert.deepEqual( + CLIENT_TRACKING.transformArguments(true, { + OPTOUT: true + }), + ['CLIENT', 'TRACKING', 'ON', 'OPTOUT'] + ); + }); - it('with NOLOOP', () => { - assert.deepEqual( - transformArguments(true, { - NOLOOP: true - }), - ['CLIENT', 'TRACKING', 'ON', 'NOLOOP'] - ); - }); - }); + it('with NOLOOP', () => { + assert.deepEqual( + CLIENT_TRACKING.transformArguments(true, { + NOLOOP: true + }), + ['CLIENT', 'TRACKING', 'ON', 'NOLOOP'] + ); + }); + }); - it('false', () => { - assert.deepEqual( - transformArguments(false), - ['CLIENT', 'TRACKING', 'OFF'] - ); - }); + it('false', () => { + assert.deepEqual( + CLIENT_TRACKING.transformArguments(false), + ['CLIENT', 'TRACKING', 'OFF'] + ); }); + }); - testUtils.testWithClient('client.clientTracking', async client => { - assert.equal( - await client.clientTracking(false), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.clientTracking', async client => { + assert.equal( + await client.clientTracking(false), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_TRACKING.ts b/packages/client/lib/commands/CLIENT_TRACKING.ts index c70702706e4..a783ce35894 100644 --- a/packages/client/lib/commands/CLIENT_TRACKING.ts +++ b/packages/client/lib/commands/CLIENT_TRACKING.ts @@ -1,83 +1,87 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; interface CommonOptions { - REDIRECT?: number; - NOLOOP?: boolean; + REDIRECT?: number; + NOLOOP?: boolean; } interface BroadcastOptions { - BCAST?: boolean; - PREFIX?: RedisCommandArgument | Array; + BCAST?: boolean; + PREFIX?: RedisVariadicArgument; } interface OptInOptions { - OPTIN?: boolean; + OPTIN?: boolean; } interface OptOutOptions { - OPTOUT?: boolean; + OPTOUT?: boolean; } -type ClientTrackingOptions = CommonOptions & ( - BroadcastOptions | - OptInOptions | - OptOutOptions +export type ClientTrackingOptions = CommonOptions & ( + BroadcastOptions | + OptInOptions | + OptOutOptions ); -export function transformArguments( +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments( mode: M, - options?: M extends true ? ClientTrackingOptions : undefined -): RedisCommandArguments { - const args: RedisCommandArguments = [ - 'CLIENT', - 'TRACKING', - mode ? 'ON' : 'OFF' + options?: M extends true ? ClientTrackingOptions : never + ) { + const args: Array = [ + 'CLIENT', + 'TRACKING', + mode ? 'ON' : 'OFF' ]; if (mode) { - if (options?.REDIRECT) { - args.push( - 'REDIRECT', - options.REDIRECT.toString() - ); - } + if (options?.REDIRECT) { + args.push( + 'REDIRECT', + options.REDIRECT.toString() + ); + } - if (isBroadcast(options)) { - args.push('BCAST'); + if (isBroadcast(options)) { + args.push('BCAST'); - if (options?.PREFIX) { - if (Array.isArray(options.PREFIX)) { - for (const prefix of options.PREFIX) { - args.push('PREFIX', prefix); - } - } else { - args.push('PREFIX', options.PREFIX); - } + if (options?.PREFIX) { + if (Array.isArray(options.PREFIX)) { + for (const prefix of options.PREFIX) { + args.push('PREFIX', prefix); } - } else if (isOptIn(options)) { - args.push('OPTIN'); - } else if (isOptOut(options)) { - args.push('OPTOUT'); + } else { + args.push('PREFIX', options.PREFIX); + } } + } else if (isOptIn(options)) { + args.push('OPTIN'); + } else if (isOptOut(options)) { + args.push('OPTOUT'); + } - if (options?.NOLOOP) { - args.push('NOLOOP'); - } + if (options?.NOLOOP) { + args.push('NOLOOP'); + } } return args; -} + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; function isBroadcast(options?: ClientTrackingOptions): options is BroadcastOptions { - return (options as BroadcastOptions)?.BCAST === true; + return (options as BroadcastOptions)?.BCAST === true; } function isOptIn(options?: ClientTrackingOptions): options is OptInOptions { - return (options as OptInOptions)?.OPTIN === true; + return (options as OptInOptions)?.OPTIN === true; } function isOptOut(options?: ClientTrackingOptions): options is OptOutOptions { - return (options as OptOutOptions)?.OPTOUT === true; + return (options as OptOutOptions)?.OPTOUT === true; } - -export declare function transformReply(): 'OK' | Buffer; diff --git a/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts b/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts index 49bffe7612d..1cefbd27d53 100644 --- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts +++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts @@ -1,25 +1,25 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLIENT_TRACKINGINFO'; +import CLIENT_TRACKINGINFO from './CLIENT_TRACKINGINFO'; describe('CLIENT TRACKINGINFO', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'TRACKINGINFO'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLIENT_TRACKINGINFO.transformArguments(), + ['CLIENT', 'TRACKINGINFO'] + ); + }); - testUtils.testWithClient('client.clientTrackingInfo', async client => { - assert.deepEqual( - await client.clientTrackingInfo(), - { - flags: new Set(['off']), - redirect: -1, - prefixes: [] - } - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.clientTrackingInfo', async client => { + assert.deepEqual( + await client.clientTrackingInfo(), + { + flags: ['off'], + redirect: -1, + prefixes: [] + } + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts index 7c883fc6997..d969ba0219e 100644 --- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts +++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts @@ -1,28 +1,23 @@ -import { RedisCommandArguments } from '.'; +import { TuplesToMapReply, BlobStringReply, SetReply, NumberReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['CLIENT', 'TRACKINGINFO']; -} - -type RawReply = [ - 'flags', - Array, - 'redirect', - number, - 'prefixes', - Array -]; +type TrackingInfo = TuplesToMapReply<[ + [BlobStringReply<'flags'>, SetReply], + [BlobStringReply<'redirect'>, NumberReply], + [BlobStringReply<'prefixes'>, ArrayReply] +]>; -interface Reply { - flags: Set; - redirect: number; - prefixes: Array; -} - -export function transformReply(reply: RawReply): Reply { - return { - flags: new Set(reply[1]), - redirect: reply[3], - prefixes: reply[5] - }; -} +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLIENT', 'TRACKINGINFO']; + }, + transformReply: { + 2: (reply: UnwrapReply>) => ({ + flags: reply[1], + redirect: reply[3], + prefixes: reply[5] + }), + 3: undefined as unknown as () => TrackingInfo + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts b/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts index 73c731ee87f..bddf3ca0f02 100644 --- a/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts +++ b/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLIENT_UNPAUSE'; +import CLIENT_UNPAUSE from './CLIENT_UNPAUSE'; describe('CLIENT UNPAUSE', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLIENT', 'UNPAUSE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLIENT_UNPAUSE.transformArguments(), + ['CLIENT', 'UNPAUSE'] + ); + }); - testUtils.testWithClient('client.unpause', async client => { - assert.equal( - await client.clientUnpause(), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.clientUnpause', async client => { + assert.equal( + await client.clientUnpause(), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLIENT_UNPAUSE.ts b/packages/client/lib/commands/CLIENT_UNPAUSE.ts index e139436d004..9da0a9a8bbe 100644 --- a/packages/client/lib/commands/CLIENT_UNPAUSE.ts +++ b/packages/client/lib/commands/CLIENT_UNPAUSE.ts @@ -1,7 +1,10 @@ -import { RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['CLIENT', 'UNPAUSE']; -} - -export declare function transformReply(): 'OK' | Buffer; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts index c16476de436..56f7b2a85e7 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_ADDSLOTS'; +import { strict as assert } from 'node:assert'; +import CLUSTER_ADDSLOTS from './CLUSTER_ADDSLOTS'; describe('CLUSTER ADDSLOTS', () => { - describe('transformArguments', () => { - it('single', () => { - assert.deepEqual( - transformArguments(0), - ['CLUSTER', 'ADDSLOTS', '0'] - ); - }); + describe('transformArguments', () => { + it('single', () => { + assert.deepEqual( + CLUSTER_ADDSLOTS.transformArguments(0), + ['CLUSTER', 'ADDSLOTS', '0'] + ); + }); - it('multiple', () => { - assert.deepEqual( - transformArguments([0, 1]), - ['CLUSTER', 'ADDSLOTS', '0', '1'] - ); - }); + it('multiple', () => { + assert.deepEqual( + CLUSTER_ADDSLOTS.transformArguments([0, 1]), + ['CLUSTER', 'ADDSLOTS', '0', '1'] + ); }); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts index 6cd357fb823..dc42c2f13e8 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts @@ -1,11 +1,14 @@ -import { RedisCommandArguments } from '.'; -import { pushVerdictNumberArguments } from './generic-transformers'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { pushVariadicNumberArguments } from './generic-transformers'; -export function transformArguments(slots: number | Array): RedisCommandArguments { - return pushVerdictNumberArguments( - ['CLUSTER', 'ADDSLOTS'], - slots +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(slots: number | Array) { + return pushVariadicNumberArguments( + ['CLUSTER', 'ADDSLOTS'], + slots ); -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts index ebd1e3445ff..6af6f586e99 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts @@ -1,29 +1,32 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_ADDSLOTSRANGE'; +import { strict as assert } from 'node:assert'; +import testUtils from '../test-utils'; +import CLUSTER_ADDSLOTSRANGE from './CLUSTER_ADDSLOTSRANGE'; describe('CLUSTER ADDSLOTSRANGE', () => { - describe('transformArguments', () => { - it('single', () => { - assert.deepEqual( - transformArguments({ - start: 0, - end: 1 - }), - ['CLUSTER', 'ADDSLOTSRANGE', '0', '1'] - ); - }); + testUtils.isVersionGreaterThanHook([7, 0]); - it('multiple', () => { - assert.deepEqual( - transformArguments([{ - start: 0, - end: 1 - }, { - start: 2, - end: 3 - }]), - ['CLUSTER', 'ADDSLOTSRANGE', '0', '1', '2', '3'] - ); - }); + describe('transformArguments', () => { + it('single', () => { + assert.deepEqual( + CLUSTER_ADDSLOTSRANGE.transformArguments({ + start: 0, + end: 1 + }), + ['CLUSTER', 'ADDSLOTSRANGE', '0', '1'] + ); }); + + it('multiple', () => { + assert.deepEqual( + CLUSTER_ADDSLOTSRANGE.transformArguments([{ + start: 0, + end: 1 + }, { + start: 2, + end: 3 + }]), + ['CLUSTER', 'ADDSLOTSRANGE', '0', '1', '2', '3'] + ); + }); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts index 6a8d6dc668f..5cf649a30da 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts @@ -1,13 +1,14 @@ -import { RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; import { pushSlotRangesArguments, SlotRange } from './generic-transformers'; -export function transformArguments( - ranges: SlotRange | Array -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(ranges: SlotRange | Array) { return pushSlotRangesArguments( - ['CLUSTER', 'ADDSLOTSRANGE'], - ranges + ['CLUSTER', 'ADDSLOTSRANGE'], + ranges ); -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts index edb68b3b3b0..d21bc47c5d0 100644 --- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts +++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_BUMPEPOCH'; +import CLUSTER_BUMPEPOCH from './CLUSTER_BUMPEPOCH'; describe('CLUSTER BUMPEPOCH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'BUMPEPOCH'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_BUMPEPOCH.transformArguments(), + ['CLUSTER', 'BUMPEPOCH'] + ); + }); - testUtils.testWithCluster('clusterNode.clusterBumpEpoch', async cluster => { - const client = await cluster.nodeClient(cluster.masters[0]); - assert.equal( - typeof await client.clusterBumpEpoch(), - 'string' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterBumpEpoch', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]); + assert.equal( + typeof await client.clusterBumpEpoch(), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts index 7f81c8fdc42..94f7e3b56f9 100644 --- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts +++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'BUMPEPOCH']; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'BUMPED' | 'STILL'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLUSTER', 'BUMPEPOCH']; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'BUMPED' | 'STILL'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts index 558110d0a28..93c2aca7804 100644 --- a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts @@ -1,22 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_COUNT-FAILURE-REPORTS'; +import CLUSTER_COUNT_FAILURE_REPORTS from './CLUSTER_COUNT-FAILURE-REPORTS'; describe('CLUSTER COUNT-FAILURE-REPORTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('0'), - ['CLUSTER', 'COUNT-FAILURE-REPORTS', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_COUNT_FAILURE_REPORTS.transformArguments('0'), + ['CLUSTER', 'COUNT-FAILURE-REPORTS', '0'] + ); + }); - testUtils.testWithCluster('clusterNode.clusterCountFailureReports', async cluster => { - const client = await cluster.nodeClient(cluster.masters[0]); - assert.equal( - typeof await client.clusterCountFailureReports( - await client.clusterMyId() - ), - 'number' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterCountFailureReports', async cluster => { + const [master] = cluster.masters, + client = await cluster.nodeClient(master); + assert.equal( + typeof await client.clusterCountFailureReports(master.id), + 'number' + ); + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts index 3fbc33052f8..a005694713d 100644 --- a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts +++ b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts @@ -1,5 +1,10 @@ -export function transformArguments(nodeId: string): Array { - return ['CLUSTER', 'COUNT-FAILURE-REPORTS', nodeId]; -} +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(nodeId: RedisArgument) { + return ['CLUSTER', 'COUNT-FAILURE-REPORTS', nodeId]; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts index 27ecbcfffa3..180a120e153 100644 --- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_COUNTKEYSINSLOT'; +import CLUSTER_COUNTKEYSINSLOT from './CLUSTER_COUNTKEYSINSLOT'; describe('CLUSTER COUNTKEYSINSLOT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0), - ['CLUSTER', 'COUNTKEYSINSLOT', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_COUNTKEYSINSLOT.transformArguments(0), + ['CLUSTER', 'COUNTKEYSINSLOT', '0'] + ); + }); - testUtils.testWithCluster('clusterNode.clusterCountKeysInSlot', async cluster => { - const client = await cluster.nodeClient(cluster.masters[0]); - assert.equal( - typeof await client.clusterCountKeysInSlot(0), - 'number' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterCountKeysInSlot', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]); + assert.equal( + typeof await client.clusterCountKeysInSlot(0), + 'number' + ); + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts index a5ff75e58a9..61f46230e89 100644 --- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts @@ -1,5 +1,10 @@ -export function transformArguments(slot: number): Array { - return ['CLUSTER', 'COUNTKEYSINSLOT', slot.toString()]; -} +import { NumberReply, Command } from '../RESP/types'; -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(slot: number) { + return ['CLUSTER', 'COUNTKEYSINSLOT', slot.toString()]; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts index 85d13f4ed3d..59e40217b9c 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_DELSLOTS'; +import { strict as assert } from 'node:assert'; +import CLUSTER_DELSLOTS from './CLUSTER_DELSLOTS'; describe('CLUSTER DELSLOTS', () => { - describe('transformArguments', () => { - it('single', () => { - assert.deepEqual( - transformArguments(0), - ['CLUSTER', 'DELSLOTS', '0'] - ); - }); + describe('transformArguments', () => { + it('single', () => { + assert.deepEqual( + CLUSTER_DELSLOTS.transformArguments(0), + ['CLUSTER', 'DELSLOTS', '0'] + ); + }); - it('multiple', () => { - assert.deepEqual( - transformArguments([0, 1]), - ['CLUSTER', 'DELSLOTS', '0', '1'] - ); - }); + it('multiple', () => { + assert.deepEqual( + CLUSTER_DELSLOTS.transformArguments([0, 1]), + ['CLUSTER', 'DELSLOTS', '0', '1'] + ); }); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts index bf8d9c18900..6a6bbb76085 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts @@ -1,11 +1,14 @@ -import { RedisCommandArguments } from '.'; -import { pushVerdictNumberArguments } from './generic-transformers'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { pushVariadicNumberArguments } from './generic-transformers'; -export function transformArguments(slots: number | Array): RedisCommandArguments { - return pushVerdictNumberArguments( - ['CLUSTER', 'DELSLOTS'], - slots +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(slots: number | Array) { + return pushVariadicNumberArguments( + ['CLUSTER', 'DELSLOTS'], + slots ); -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts index 8fd50d01a54..2615f394b87 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts @@ -1,29 +1,29 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_DELSLOTSRANGE'; +import { strict as assert } from 'node:assert'; +import CLUSTER_DELSLOTSRANGE from './CLUSTER_DELSLOTSRANGE'; describe('CLUSTER DELSLOTSRANGE', () => { - describe('transformArguments', () => { - it('single', () => { - assert.deepEqual( - transformArguments({ - start: 0, - end: 1 - }), - ['CLUSTER', 'DELSLOTSRANGE', '0', '1'] - ); - }); + describe('transformArguments', () => { + it('single', () => { + assert.deepEqual( + CLUSTER_DELSLOTSRANGE.transformArguments({ + start: 0, + end: 1 + }), + ['CLUSTER', 'DELSLOTSRANGE', '0', '1'] + ); + }); - it('multiple', () => { - assert.deepEqual( - transformArguments([{ - start: 0, - end: 1 - }, { - start: 2, - end: 3 - }]), - ['CLUSTER', 'DELSLOTSRANGE', '0', '1', '2', '3'] - ); - }); + it('multiple', () => { + assert.deepEqual( + CLUSTER_DELSLOTSRANGE.transformArguments([{ + start: 0, + end: 1 + }, { + start: 2, + end: 3 + }]), + ['CLUSTER', 'DELSLOTSRANGE', '0', '1', '2', '3'] + ); }); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts index b136113c65f..e28ca9c8405 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts @@ -1,13 +1,14 @@ -import { RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; import { pushSlotRangesArguments, SlotRange } from './generic-transformers'; -export function transformArguments( - ranges: SlotRange | Array -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(ranges: SlotRange | Array) { return pushSlotRangesArguments( - ['CLUSTER', 'DELSLOTSRANGE'], - ranges + ['CLUSTER', 'DELSLOTSRANGE'], + ranges ); -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts b/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts index 578ff56b9cd..ac18a9a7f8f 100644 --- a/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts +++ b/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts @@ -1,20 +1,22 @@ -import { strict as assert } from 'assert'; -import { FailoverModes, transformArguments } from './CLUSTER_FAILOVER'; +import { strict as assert } from 'node:assert'; +import CLUSTER_FAILOVER, { FAILOVER_MODES } from './CLUSTER_FAILOVER'; describe('CLUSTER FAILOVER', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'FAILOVER'] - ); - }); - - it('with mode', () => { - assert.deepEqual( - transformArguments(FailoverModes.FORCE), - ['CLUSTER', 'FAILOVER', 'FORCE'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + CLUSTER_FAILOVER.transformArguments(), + ['CLUSTER', 'FAILOVER'] + ); }); + + it('with mode', () => { + assert.deepEqual( + CLUSTER_FAILOVER.transformArguments({ + mode: FAILOVER_MODES.FORCE + }), + ['CLUSTER', 'FAILOVER', 'FORCE'] + ); + }); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_FAILOVER.ts b/packages/client/lib/commands/CLUSTER_FAILOVER.ts index 9bc4b69f343..63f79a246ba 100644 --- a/packages/client/lib/commands/CLUSTER_FAILOVER.ts +++ b/packages/client/lib/commands/CLUSTER_FAILOVER.ts @@ -1,16 +1,27 @@ -export enum FailoverModes { - FORCE = 'FORCE', - TAKEOVER = 'TAKEOVER' +import { SimpleStringReply, Command } from '../RESP/types'; + +export const FAILOVER_MODES = { + FORCE: 'FORCE', + TAKEOVER: 'TAKEOVER' +} as const; + +export type FailoverMode = typeof FAILOVER_MODES[keyof typeof FAILOVER_MODES]; + +export interface ClusterFailoverOptions { + mode?: FailoverMode; } -export function transformArguments(mode?: FailoverModes): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(options?: ClusterFailoverOptions) { const args = ['CLUSTER', 'FAILOVER']; - if (mode) { - args.push(mode); + if (options?.mode) { + args.push(options.mode); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts index f91a9a70cfd..fbc4346136d 100644 --- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_FLUSHSLOTS'; +import { strict as assert } from 'node:assert'; +import CLUSTER_FLUSHSLOTS from './CLUSTER_FLUSHSLOTS'; describe('CLUSTER FLUSHSLOTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'FLUSHSLOTS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_FLUSHSLOTS.transformArguments(), + ['CLUSTER', 'FLUSHSLOTS'] + ); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts index dfb1e1ccde8..327ed7b7d17 100644 --- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'FLUSHSLOTS']; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLUSTER', 'FLUSHSLOTS']; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_FORGET.spec.ts b/packages/client/lib/commands/CLUSTER_FORGET.spec.ts index cadcdb678f3..a9a923b01ee 100644 --- a/packages/client/lib/commands/CLUSTER_FORGET.spec.ts +++ b/packages/client/lib/commands/CLUSTER_FORGET.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_FORGET'; +import { strict as assert } from 'node:assert'; +import CLUSTER_FORGET from './CLUSTER_FORGET'; describe('CLUSTER FORGET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('0'), - ['CLUSTER', 'FORGET', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_FORGET.transformArguments('0'), + ['CLUSTER', 'FORGET', '0'] + ); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_FORGET.ts b/packages/client/lib/commands/CLUSTER_FORGET.ts index fc557073aeb..a51c039563b 100644 --- a/packages/client/lib/commands/CLUSTER_FORGET.ts +++ b/packages/client/lib/commands/CLUSTER_FORGET.ts @@ -1,5 +1,10 @@ -export function transformArguments(nodeId: string): Array { - return ['CLUSTER', 'FORGET', nodeId]; -} +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(nodeId: RedisArgument) { + return ['CLUSTER', 'FORGET', nodeId]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts index 957b7de20cb..f1a4e2c3bcc 100644 --- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts @@ -1,21 +1,25 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_GETKEYSINSLOT'; +import CLUSTER_GETKEYSINSLOT from './CLUSTER_GETKEYSINSLOT'; describe('CLUSTER GETKEYSINSLOT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0, 10), - ['CLUSTER', 'GETKEYSINSLOT', '0', '10'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_GETKEYSINSLOT.transformArguments(0, 10), + ['CLUSTER', 'GETKEYSINSLOT', '0', '10'] + ); + }); - testUtils.testWithCluster('clusterNode.clusterGetKeysInSlot', async cluster => { - const client = await cluster.nodeClient(cluster.masters[0]), - reply = await client.clusterGetKeysInSlot(0, 1); - assert.ok(Array.isArray(reply)); - for (const item of reply) { - assert.equal(typeof item, 'string'); - } - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterGetKeysInSlot', async cluster => { + const slot = 12539, // "key" slot + client = await cluster.nodeClient(cluster.slots[slot].master), + [, reply] = await Promise.all([ + client.set('key', 'value'), + client.clusterGetKeysInSlot(slot, 1), + ]) + assert.ok(Array.isArray(reply)); + for (const item of reply) { + assert.equal(typeof item, 'string'); + } + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts index ec75b7b7336..c19cd225e04 100644 --- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts @@ -1,5 +1,10 @@ -export function transformArguments(slot: number, count: number): Array { - return ['CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()]; -} +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(slot: number, count: number) { + return ['CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()]; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_INFO.spec.ts b/packages/client/lib/commands/CLUSTER_INFO.spec.ts index 69d5c4a8c56..f7c708663fc 100644 --- a/packages/client/lib/commands/CLUSTER_INFO.spec.ts +++ b/packages/client/lib/commands/CLUSTER_INFO.spec.ts @@ -1,55 +1,20 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './CLUSTER_INFO'; +import CLUSTER_INFO from './CLUSTER_INFO'; describe('CLUSTER INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'INFO'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_INFO.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 - } - } - ); - }); - - testUtils.testWithCluster('clusterNode.clusterInfo', async cluster => { - const client = await cluster.nodeClient(cluster.masters[0]); - assert.notEqual( - await client.clusterInfo(), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterInfo', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]); + assert.equal( + typeof await client.clusterInfo(), + 'string' + ); + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_INFO.ts b/packages/client/lib/commands/CLUSTER_INFO.ts index 634515f927c..4605efbe81a 100644 --- a/packages/client/lib/commands/CLUSTER_INFO.ts +++ b/packages/client/lib/commands/CLUSTER_INFO.ts @@ -1,47 +1,10 @@ -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'); +import { VerbatimStringReply, Command } from '../RESP/types'; - 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); -} +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLUSTER', 'INFO']; + }, + transformReply: undefined as unknown as () => VerbatimStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts index 3bbc9f9cb2d..d582c616cd1 100644 --- a/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_KEYSLOT'; +import CLUSTER_KEYSLOT from './CLUSTER_KEYSLOT'; describe('CLUSTER KEYSLOT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['CLUSTER', 'KEYSLOT', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_KEYSLOT.transformArguments('key'), + ['CLUSTER', 'KEYSLOT', 'key'] + ); + }); - testUtils.testWithCluster('clusterNode.clusterKeySlot', async cluster => { - const client = await cluster.nodeClient(cluster.masters[0]); - assert.equal( - typeof await client.clusterKeySlot('key'), - 'number' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterKeySlot', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]); + assert.equal( + typeof await client.clusterKeySlot('key'), + 'number' + ); + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts index 0af524ff128..81e84430116 100644 --- a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts @@ -1,5 +1,10 @@ -export function transformArguments(key: string): Array { - return ['CLUSTER', 'KEYSLOT', key]; -} +import { Command, NumberReply, RedisArgument } from '../RESP/types'; -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { + return ['CLUSTER', 'KEYSLOT', key]; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_LINKS.spec.ts b/packages/client/lib/commands/CLUSTER_LINKS.spec.ts index 982973e8ea5..d94231634e0 100644 --- a/packages/client/lib/commands/CLUSTER_LINKS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_LINKS.spec.ts @@ -1,28 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_LINKS'; +import CLUSTER_LINKS from './CLUSTER_LINKS'; describe('CLUSTER LINKS', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'LINKS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_LINKS.transformArguments(), + ['CLUSTER', 'LINKS'] + ); + }); - testUtils.testWithCluster('clusterNode.clusterLinks', async cluster => { - const client = await cluster.nodeClient(cluster.masters[0]), - links = await client.clusterLinks(); - assert.ok(Array.isArray(links)); - for (const link of links) { - assert.equal(typeof link.direction, 'string'); - assert.equal(typeof link.node, 'string'); - assert.equal(typeof link.createTime, 'number'); - assert.equal(typeof link.events, 'string'); - assert.equal(typeof link.sendBufferAllocated, 'number'); - assert.equal(typeof link.sendBufferUsed, 'number'); - } - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterLinks', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]), + links = await client.clusterLinks(); + assert.ok(Array.isArray(links)); + for (const link of links) { + assert.equal(typeof link.direction, 'string'); + assert.equal(typeof link.node, 'string'); + assert.equal(typeof link['create-time'], 'number'); + assert.equal(typeof link.events, 'string'); + assert.equal(typeof link['send-buffer-allocated'], 'number'); + assert.equal(typeof link['send-buffer-used'], 'number'); + } + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_LINKS.ts b/packages/client/lib/commands/CLUSTER_LINKS.ts index 9a5608c102f..df83f3f7a11 100644 --- a/packages/client/lib/commands/CLUSTER_LINKS.ts +++ b/packages/client/lib/commands/CLUSTER_LINKS.ts @@ -1,38 +1,32 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'LINKS']; -} - -type ClusterLinksRawReply = Array<[ - 'direction', - string, - 'node', - string, - 'createTime', - number, - 'events', - string, - 'send-buffer-allocated', - number, - 'send-buffer-used', - number -]>; +import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -type ClusterLinksReply = Array<{ - direction: string; - node: string; - createTime: number; - events: string; - sendBufferAllocated: number; - sendBufferUsed: number; -}>; +type ClusterLinksReply = ArrayReply, BlobStringReply], + [BlobStringReply<'node'>, BlobStringReply], + [BlobStringReply<'create-time'>, NumberReply], + [BlobStringReply<'events'>, BlobStringReply], + [BlobStringReply<'send-buffer-allocated'>, NumberReply], + [BlobStringReply<'send-buffer-used'>, NumberReply], +]>>; -export function transformReply(reply: ClusterLinksRawReply): ClusterLinksReply { - return reply.map(peerLink => ({ - direction: peerLink[1], - node: peerLink[3], - createTime: Number(peerLink[5]), - events: peerLink[7], - sendBufferAllocated: Number(peerLink[9]), - sendBufferUsed: Number(peerLink[11]) - })); -} +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLUSTER', 'LINKS']; + }, + transformReply: { + 2: (reply: UnwrapReply>) => reply.map(link => { + const unwrapped = link as unknown as UnwrapReply; + return { + direction: unwrapped[1], + node: unwrapped[3], + 'create-time': unwrapped[5], + events: unwrapped[7], + 'send-buffer-allocated': unwrapped[9], + 'send-buffer-used': unwrapped[11] + }; + }), + 3: undefined as unknown as () => ClusterLinksReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_MEET.spec.ts b/packages/client/lib/commands/CLUSTER_MEET.spec.ts index 50a5393efa2..0b678f009f7 100644 --- a/packages/client/lib/commands/CLUSTER_MEET.spec.ts +++ b/packages/client/lib/commands/CLUSTER_MEET.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_MEET'; +import { strict as assert } from 'node:assert'; +import CLUSTER_MEET from './CLUSTER_MEET'; describe('CLUSTER MEET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('127.0.0.1', 6379), - ['CLUSTER', 'MEET', '127.0.0.1', '6379'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_MEET.transformArguments('127.0.0.1', 6379), + ['CLUSTER', 'MEET', '127.0.0.1', '6379'] + ); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_MEET.ts b/packages/client/lib/commands/CLUSTER_MEET.ts index e6ce1c1fce4..df72599d40b 100644 --- a/packages/client/lib/commands/CLUSTER_MEET.ts +++ b/packages/client/lib/commands/CLUSTER_MEET.ts @@ -1,5 +1,10 @@ -export function transformArguments(ip: string, port: number): Array { - return ['CLUSTER', 'MEET', ip, port.toString()]; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(host: string, port: number) { + return ['CLUSTER', 'MEET', host, port.toString()]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_MYID.spec.ts b/packages/client/lib/commands/CLUSTER_MYID.spec.ts index f427d7058e2..74540e98ab7 100644 --- a/packages/client/lib/commands/CLUSTER_MYID.spec.ts +++ b/packages/client/lib/commands/CLUSTER_MYID.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_MYID'; +import CLUSTER_MYID from './CLUSTER_MYID'; describe('CLUSTER MYID', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'MYID'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_MYID.transformArguments(), + ['CLUSTER', 'MYID'] + ); + }); - testUtils.testWithCluster('clusterNode.clusterMyId', async cluster => { - const [master] = cluster.masters, - client = await cluster.nodeClient(master); - assert.equal( - await client.clusterMyId(), - master.id - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterMyId', async cluster => { + const [master] = cluster.masters, + client = await cluster.nodeClient(master); + assert.equal( + await client.clusterMyId(), + master.id + ); + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_MYID.ts b/packages/client/lib/commands/CLUSTER_MYID.ts index 2b61684634d..73711b47ebb 100644 --- a/packages/client/lib/commands/CLUSTER_MYID.ts +++ b/packages/client/lib/commands/CLUSTER_MYID.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'MYID']; -} +import { BlobStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLUSTER', 'MYID']; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts b/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts index 180289870ca..e64f2e3777a 100644 --- a/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts +++ b/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts @@ -1,22 +1,22 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_MYSHARDID'; +import CLUSTER_MYSHARDID from './CLUSTER_MYSHARDID'; describe('CLUSTER MYSHARDID', () => { - testUtils.isVersionGreaterThanHook([7, 2]); + testUtils.isVersionGreaterThanHook([7, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'MYSHARDID'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_MYSHARDID.transformArguments(), + ['CLUSTER', 'MYSHARDID'] + ); + }); - testUtils.testWithCluster('clusterNode.clusterMyShardId', async cluster => { - const client = await cluster.nodeClient(cluster.masters[0]); - assert.equal( - typeof await client.clusterMyShardId(), - 'string' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterMyShardId', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]); + assert.equal( + typeof await client.clusterMyShardId(), + 'string' + ); + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_MYSHARDID.ts b/packages/client/lib/commands/CLUSTER_MYSHARDID.ts index 1c4f8b82f56..0c38b61634f 100644 --- a/packages/client/lib/commands/CLUSTER_MYSHARDID.ts +++ b/packages/client/lib/commands/CLUSTER_MYSHARDID.ts @@ -1,7 +1,11 @@ -export const IS_READ_ONLY = true; +import { BlobStringReply, Command } from '../RESP/types'; -export function transformArguments() { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['CLUSTER', 'MYSHARDID']; -} + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; -export declare function transformReply(): string | Buffer; diff --git a/packages/client/lib/commands/CLUSTER_NODES.spec.ts b/packages/client/lib/commands/CLUSTER_NODES.spec.ts index 5c6cb74d6cb..99db17a23e6 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.spec.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.spec.ts @@ -1,145 +1,20 @@ -import { strict as assert } from 'assert'; -import { RedisClusterNodeLinkStates, transformArguments, transformReply } from './CLUSTER_NODES'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import CLUSTER_NODES from './CLUSTER_NODES'; describe('CLUSTER NODES', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'NODES'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_NODES.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', - address: '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', - address: '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('should support addresses without cport', () => { - assert.deepEqual( - transformReply( - 'id 127.0.0.1:30001 master - 0 0 0 connected 0-16384\n' - ), - [{ - id: 'id', - address: '127.0.0.1:30001', - host: '127.0.0.1', - port: 30001, - cport: null, - flags: ['master'], - pingSent: 0, - pongRecv: 0, - configEpoch: 0, - linkState: RedisClusterNodeLinkStates.CONNECTED, - slots: [{ - from: 0, - to: 16384 - }], - replicas: [] - }] - ); - }); - - it('should support ipv6 addresses', () => { - assert.deepEqual( - transformReply( - 'id 2a02:6b8:c21:330d:0:1589:ebbe:b1a0:6379@16379 master - 0 0 0 connected 0-549\n' - ), - [{ - id: 'id', - address: '2a02:6b8:c21:330d:0:1589:ebbe:b1a0:6379@16379', - host: '2a02:6b8:c21:330d:0:1589:ebbe:b1a0', - port: 6379, - cport: 16379, - flags: ['master'], - pingSent: 0, - pongRecv: 0, - configEpoch: 0, - linkState: RedisClusterNodeLinkStates.CONNECTED, - slots: [{ - from: 0, - to: 549 - }], - replicas: [] - }] - ); - }); - - 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', - address: '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', - address: '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: [] - }] - ); - }); - }); + testUtils.testWithCluster('clusterNode.clusterNodes', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]); + assert.equal( + typeof await client.clusterNodes(), + 'string' + ); + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_NODES.ts b/packages/client/lib/commands/CLUSTER_NODES.ts index 7c433da5f12..64dd5056232 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.ts @@ -1,105 +1,10 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'NODES']; -} - -export enum RedisClusterNodeLinkStates { - CONNECTED = 'connected', - DISCONNECTED = 'disconnected' -} - -interface RedisClusterNodeAddress { - host: string; - port: number; - cport: number | null; -} - -export interface RedisClusterReplicaNode extends RedisClusterNodeAddress { - id: string; - address: 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>(); +import { VerbatimStringReply, Command } from '../RESP/types'; - for (const line of lines) { - const [id, address, flags, masterId, pingSent, pongRecv, configEpoch, linkState, ...slots] = line.split(' '), - node = { - id, - address, - ...transformNodeAddress(address), - 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 transformNodeAddress(address: string): RedisClusterNodeAddress { - const indexOfColon = address.lastIndexOf(':'), - indexOfAt = address.indexOf('@', indexOfColon), - host = address.substring(0, indexOfColon); - - if (indexOfAt === -1) { - return { - host, - port: Number(address.substring(indexOfColon + 1)), - cport: null - }; - } - - return { - host: address.substring(0, indexOfColon), - port: Number(address.substring(indexOfColon + 1, indexOfAt)), - cport: Number(address.substring(indexOfAt + 1)) - }; -} +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLUSTER', 'NODES']; + }, + transformReply: undefined as unknown as () => VerbatimStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts b/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts index 6c902dc0d82..1a48f360885 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts @@ -1,11 +1,21 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_REPLICAS'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import CLUSTER_REPLICAS from './CLUSTER_REPLICAS'; describe('CLUSTER REPLICAS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('0'), - ['CLUSTER', 'REPLICAS', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_REPLICAS.transformArguments('0'), + ['CLUSTER', 'REPLICAS', '0'] + ); + }); + + testUtils.testWithCluster('clusterNode.clusterReplicas', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]), + reply = await client.clusterReplicas(cluster.masters[0].id); + assert.ok(Array.isArray(reply)); + for (const replica of reply) { + assert.equal(typeof replica, 'string'); + } + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_REPLICAS.ts b/packages/client/lib/commands/CLUSTER_REPLICAS.ts index a4130125fbf..8e0fe2cdfd9 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICAS.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICAS.ts @@ -1,5 +1,10 @@ -export function transformArguments(nodeId: string): Array { - return ['CLUSTER', 'REPLICAS', nodeId]; -} +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export { transformReply } from './CLUSTER_NODES'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(nodeId: RedisArgument) { + return ['CLUSTER', 'REPLICAS', nodeId]; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts b/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts index 926b7dd0a77..80935385a88 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_REPLICATE'; +import { strict as assert } from 'node:assert'; +import CLUSTER_REPLICATE from './CLUSTER_REPLICATE'; describe('CLUSTER REPLICATE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('0'), - ['CLUSTER', 'REPLICATE', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_REPLICATE.transformArguments('0'), + ['CLUSTER', 'REPLICATE', '0'] + ); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_REPLICATE.ts b/packages/client/lib/commands/CLUSTER_REPLICATE.ts index c74e1ec5960..7431142024c 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICATE.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICATE.ts @@ -1,5 +1,10 @@ -export function transformArguments(nodeId: string): Array { - return ['CLUSTER', 'REPLICATE', nodeId]; -} +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(nodeId: RedisArgument) { + return ['CLUSTER', 'REPLICATE', nodeId]; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_RESET.spec.ts b/packages/client/lib/commands/CLUSTER_RESET.spec.ts index 340da7457c1..190bdaf69e1 100644 --- a/packages/client/lib/commands/CLUSTER_RESET.spec.ts +++ b/packages/client/lib/commands/CLUSTER_RESET.spec.ts @@ -1,20 +1,22 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_RESET'; +import { strict as assert } from 'node:assert'; +import CLUSTER_RESET from './CLUSTER_RESET'; describe('CLUSTER RESET', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'RESET'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + CLUSTER_RESET.transformArguments(), + ['CLUSTER', 'RESET'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments('HARD'), - ['CLUSTER', 'RESET', 'HARD'] - ); - }); + it('with mode', () => { + assert.deepEqual( + CLUSTER_RESET.transformArguments({ + mode: 'HARD' + }), + ['CLUSTER', 'RESET', 'HARD'] + ); }); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_RESET.ts b/packages/client/lib/commands/CLUSTER_RESET.ts index c6901e045dc..7aaac9d3b0d 100644 --- a/packages/client/lib/commands/CLUSTER_RESET.ts +++ b/packages/client/lib/commands/CLUSTER_RESET.ts @@ -1,11 +1,20 @@ -export function transformArguments(mode?: 'HARD' | 'SOFT'): Array { +import { SimpleStringReply, Command } from '../RESP/types'; + +export interface ClusterResetOptions { + mode?: 'HARD' | 'SOFT'; +} + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(options?: ClusterResetOptions) { const args = ['CLUSTER', 'RESET']; - if (mode) { - args.push(mode); + if (options?.mode) { + args.push(options.mode); } return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts b/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts index 81ba4aa2509..ece8087e8e4 100644 --- a/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CLUSTER_SAVECONFIG'; +import CLUSTER_SAVECONFIG from './CLUSTER_SAVECONFIG'; describe('CLUSTER SAVECONFIG', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'SAVECONFIG'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_SAVECONFIG.transformArguments(), + ['CLUSTER', 'SAVECONFIG'] + ); + }); - testUtils.testWithCluster('clusterNode.clusterSaveConfig', async cluster => { - const client = await cluster.nodeClient(cluster.masters[0]); - assert.equal( - await client.clusterSaveConfig(), - 'OK' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testWithCluster('clusterNode.clusterSaveConfig', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]); + assert.equal( + await client.clusterSaveConfig(), + 'OK' + ); + }, GLOBAL.CLUSTERS.OPEN); }); diff --git a/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts b/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts index 7e7fb181cc6..489ffd27e48 100644 --- a/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts +++ b/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { - return ['CLUSTER', 'SAVECONFIG']; -} +import { SimpleStringReply, Command } from '../RESP/types'; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLUSTER', 'SAVECONFIG']; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; -export declare function transformReply(): 'OK'; diff --git a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts index dd241574168..39cf026d0ef 100644 --- a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CLUSTER_SET-CONFIG-EPOCH'; +import { strict as assert } from 'node:assert'; +import CLUSTER_SET_CONFIG_EPOCH from './CLUSTER_SET-CONFIG-EPOCH'; describe('CLUSTER SET-CONFIG-EPOCH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0), - ['CLUSTER', 'SET-CONFIG-EPOCH', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_SET_CONFIG_EPOCH.transformArguments(0), + ['CLUSTER', 'SET-CONFIG-EPOCH', '0'] + ); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts index c50a6b9d3a5..2a650840c44 100644 --- a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts +++ b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts @@ -1,5 +1,10 @@ -export function transformArguments(configEpoch: number): Array { - return ['CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString()]; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(configEpoch: number) { + return ['CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString() ]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts index 0f46aafd13e..7bce6d74b4a 100644 --- a/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; -import { ClusterSlotStates, transformArguments } from './CLUSTER_SETSLOT'; +import { strict as assert } from 'node:assert'; +import CLUSTER_SETSLOT, { CLUSTER_SLOT_STATES } from './CLUSTER_SETSLOT'; describe('CLUSTER SETSLOT', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(0, ClusterSlotStates.IMPORTING), - ['CLUSTER', 'SETSLOT', '0', 'IMPORTING'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + CLUSTER_SETSLOT.transformArguments(0, CLUSTER_SLOT_STATES.IMPORTING), + ['CLUSTER', 'SETSLOT', '0', 'IMPORTING'] + ); + }); - it('with nodeId', () => { - assert.deepEqual( - transformArguments(0, ClusterSlotStates.IMPORTING, 'nodeId'), - ['CLUSTER', 'SETSLOT', '0', 'IMPORTING', 'nodeId'] - ); - }); + it('with nodeId', () => { + assert.deepEqual( + CLUSTER_SETSLOT.transformArguments(0, CLUSTER_SLOT_STATES.IMPORTING, 'nodeId'), + ['CLUSTER', 'SETSLOT', '0', 'IMPORTING', 'nodeId'] + ); }); + }); }); diff --git a/packages/client/lib/commands/CLUSTER_SETSLOT.ts b/packages/client/lib/commands/CLUSTER_SETSLOT.ts index c01505c71a3..ad04513688e 100644 --- a/packages/client/lib/commands/CLUSTER_SETSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_SETSLOT.ts @@ -1,22 +1,25 @@ -export enum ClusterSlotStates { - IMPORTING = 'IMPORTING', - MIGRATING = 'MIGRATING', - STABLE = 'STABLE', - NODE = 'NODE' -} +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments( - slot: number, - state: ClusterSlotStates, - nodeId?: string -): Array { - const args = ['CLUSTER', 'SETSLOT', slot.toString(), state]; +export const CLUSTER_SLOT_STATES = { + IMPORTING: 'IMPORTING', + MIGRATING: 'MIGRATING', + STABLE: 'STABLE', + NODE: 'NODE' +} as const; + +export type ClusterSlotState = typeof CLUSTER_SLOT_STATES[keyof typeof CLUSTER_SLOT_STATES]; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(slot: number, state: ClusterSlotState, nodeId?: RedisArgument) { + const args: Array = ['CLUSTER', 'SETSLOT', slot.toString(), state]; if (nodeId) { - args.push(nodeId); + args.push(nodeId); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts index 6efbfe13ce1..198dfdc6c1b 100644 --- a/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts @@ -1,76 +1,30 @@ -import { strict as assert } from 'assert'; -import { transformArguments, transformReply } from './CLUSTER_SLOTS'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import CLUSTER_SLOTS from './CLUSTER_SLOTS'; describe('CLUSTER SLOTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CLUSTER', 'SLOTS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CLUSTER_SLOTS.transformArguments(), + ['CLUSTER', 'SLOTS'] + ); + }); - it('transformReply', () => { - assert.deepEqual( - transformReply([ - [ - 0, - 5460, - ['127.0.0.1', 30001, '09dbe9720cda62f7865eabc5fd8857c5d2678366'], - ['127.0.0.1', 30004, '821d8ca00d7ccf931ed3ffc7e3db0599d2271abf'] - ], - [ - 5461, - 10922, - ['127.0.0.1', 30002, 'c9d93d9f2c0c524ff34cc11838c2003d8c29e013'], - ['127.0.0.1', 30005, 'faadb3eb99009de4ab72ad6b6ed87634c7ee410f'] - ], - [ - 10923, - 16383, - ['127.0.0.1', 30003, '044ec91f325b7595e76dbcb18cc688b6a5b434a1'], - ['127.0.0.1', 30006, '58e6e48d41228013e5d9c1c37c5060693925e97e'] - ] - ]), - [{ - from: 0, - to: 5460, - master: { - ip: '127.0.0.1', - port: 30001, - id: '09dbe9720cda62f7865eabc5fd8857c5d2678366' - }, - replicas: [{ - ip: '127.0.0.1', - port: 30004, - id: '821d8ca00d7ccf931ed3ffc7e3db0599d2271abf' - }] - }, { - from: 5461, - to: 10922, - master: { - ip: '127.0.0.1', - port: 30002, - id: 'c9d93d9f2c0c524ff34cc11838c2003d8c29e013' - }, - replicas: [{ - ip: '127.0.0.1', - port: 30005, - id: 'faadb3eb99009de4ab72ad6b6ed87634c7ee410f' - }] - }, { - from: 10923, - to: 16383, - master: { - ip: '127.0.0.1', - port: 30003, - id: '044ec91f325b7595e76dbcb18cc688b6a5b434a1' - }, - replicas: [{ - ip: '127.0.0.1', - port: 30006, - id: '58e6e48d41228013e5d9c1c37c5060693925e97e' - }] - }] - ); - }); + testUtils.testWithCluster('clusterNode.clusterSlots', async cluster => { + const client = await cluster.nodeClient(cluster.masters[0]), + slots = await client.clusterSlots(); + assert.ok(Array.isArray(slots)); + for (const { from, to, master, replicas } of slots) { + assert.equal(typeof from, 'number'); + assert.equal(typeof to, 'number'); + assert.equal(typeof master.host, 'string'); + assert.equal(typeof master.port, 'number'); + assert.equal(typeof master.id, 'string'); + for (const replica of replicas) { + assert.equal(typeof replica.host, 'string'); + assert.equal(typeof replica.port, 'number'); + assert.equal(typeof replica.id, 'string'); + } + } + }, GLOBAL.CLUSTERS.WITH_REPLICAS); }); diff --git a/packages/client/lib/commands/CLUSTER_SLOTS.ts b/packages/client/lib/commands/CLUSTER_SLOTS.ts index 20d9782dd9e..1b523328bbb 100644 --- a/packages/client/lib/commands/CLUSTER_SLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_SLOTS.ts @@ -1,46 +1,41 @@ -import { RedisCommandArguments } from '.'; +import { TuplesReply, BlobStringReply, NumberReply, ArrayReply, UnwrapReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { - return ['CLUSTER', 'SLOTS']; -} - -type ClusterSlotsRawNode = [ip: string, port: number, id: string]; - -type ClusterSlotsRawReply = Array<[ - from: number, - to: number, - master: ClusterSlotsRawNode, - ...replicas: Array +type RawNode = TuplesReply<[ + host: BlobStringReply, + port: NumberReply, + id: BlobStringReply ]>; -export interface ClusterSlotsNode { - ip: string; - port: number; - id: string; -}; +type ClusterSlotsRawReply = ArrayReply<[ + from: NumberReply, + to: NumberReply, + master: RawNode, + ...replicas: Array +]>; -export type ClusterSlotsReply = Array<{ - from: number; - to: number; - master: ClusterSlotsNode; - replicas: Array; -}>; +export type ClusterSlotsNode = ReturnType; -export function transformReply(reply: ClusterSlotsRawReply): ClusterSlotsReply { - return reply.map(([from, to, master, ...replicas]) => { - return { - from, - to, - master: transformNode(master), - replicas: replicas.map(transformNode) - }; - }); -} +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CLUSTER', 'SLOTS']; + }, + transformReply(reply: UnwrapReply) { + return reply.map(([from, to, master, ...replicas]) => ({ + from, + to, + master: transformNode(master), + replicas: replicas.map(transformNode) + })); + } +} as const satisfies Command; -function transformNode([ip, port, id]: ClusterSlotsRawNode): ClusterSlotsNode { - return { - ip, - port, - id - }; +function transformNode(node: RawNode) { + const [host, port, id] = node as unknown as UnwrapReply; + return { + host, + port, + id + }; } diff --git a/packages/client/lib/commands/COMMAND.spec.ts b/packages/client/lib/commands/COMMAND.spec.ts index baad79845ab..860ffc30685 100644 --- a/packages/client/lib/commands/COMMAND.spec.ts +++ b/packages/client/lib/commands/COMMAND.spec.ts @@ -1,17 +1,17 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './COMMAND'; -import { assertPingCommand } from './COMMAND_INFO.spec'; +// import { strict as assert } from 'node:assert'; +// import testUtils, { GLOBAL } from '../test-utils'; +// import { transformArguments } from './COMMAND'; +// import { assertPingCommand } from './COMMAND_INFO.spec'; -describe('COMMAND', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['COMMAND'] - ); - }); +// describe('COMMAND', () => { +// it('transformArguments', () => { +// assert.deepEqual( +// transformArguments(), +// ['COMMAND'] +// ); +// }); - testUtils.testWithClient('client.command', async client => { - assertPingCommand((await client.command()).find(command => command.name === 'ping')); - }, GLOBAL.SERVERS.OPEN); -}); +// testUtils.testWithClient('client.command', async client => { +// assertPingCommand((await client.command()).find(command => command.name === 'ping')); +// }, GLOBAL.SERVERS.OPEN); +// }); diff --git a/packages/client/lib/commands/COMMAND.ts b/packages/client/lib/commands/COMMAND.ts index b6ee50b2f4c..d9a960107a2 100644 --- a/packages/client/lib/commands/COMMAND.ts +++ b/packages/client/lib/commands/COMMAND.ts @@ -1,12 +1,13 @@ -import { RedisCommandArguments } from '.'; +import { ArrayReply, Command, UnwrapReply } from '../RESP/types'; import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments(): RedisCommandArguments { +export default { + IS_READ_ONLY: true, + transformArguments() { return ['COMMAND']; -} - -export function transformReply(reply: Array): Array { + }, + // TODO: This works, as we don't currently handle any of the items returned as a map + transformReply(reply: UnwrapReply>): Array { return reply.map(transformCommandReply); -} + } +} as const satisfies Command; \ No newline at end of file diff --git a/packages/client/lib/commands/COMMAND_COUNT.spec.ts b/packages/client/lib/commands/COMMAND_COUNT.spec.ts index 71482382f67..05bd29f223c 100644 --- a/packages/client/lib/commands/COMMAND_COUNT.spec.ts +++ b/packages/client/lib/commands/COMMAND_COUNT.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './COMMAND_COUNT'; +import COMMAND_COUNT from './COMMAND_COUNT'; describe('COMMAND COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['COMMAND', 'COUNT'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + COMMAND_COUNT.transformArguments(), + ['COMMAND', 'COUNT'] + ); + }); - testUtils.testWithClient('client.commandCount', async client => { - assert.equal( - typeof await client.commandCount(), - 'number' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.commandCount', async client => { + assert.equal( + typeof await client.commandCount(), + 'number' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/COMMAND_COUNT.ts b/packages/client/lib/commands/COMMAND_COUNT.ts index 34c6a088da6..10b0fdefe09 100644 --- a/packages/client/lib/commands/COMMAND_COUNT.ts +++ b/packages/client/lib/commands/COMMAND_COUNT.ts @@ -1,9 +1,10 @@ -import { RedisCommandArguments } from '.'; +import { NumberReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['COMMAND', 'COUNT']; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts b/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts index a92d032c5d6..d5b9f60790d 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './COMMAND_GETKEYS'; +import COMMAND_GETKEYS from './COMMAND_GETKEYS'; describe('COMMAND GETKEYS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(['GET', 'key']), - ['COMMAND', 'GETKEYS', 'GET', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + COMMAND_GETKEYS.transformArguments(['GET', 'key']), + ['COMMAND', 'GETKEYS', 'GET', 'key'] + ); + }); - testUtils.testWithClient('client.commandGetKeys', async client => { - assert.deepEqual( - await client.commandGetKeys(['GET', 'key']), - ['key'] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.commandGetKeys', async client => { + assert.deepEqual( + await client.commandGetKeys(['GET', 'key']), + ['key'] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/COMMAND_GETKEYS.ts b/packages/client/lib/commands/COMMAND_GETKEYS.ts index 6762fe4b58a..55cca415b8d 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYS.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(args: Array): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(args: Array) { return ['COMMAND', 'GETKEYS', ...args]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.spec.ts b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.spec.ts index d568ed0e508..49652762d65 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.spec.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.spec.ts @@ -1,24 +1,24 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './COMMAND_GETKEYSANDFLAGS'; +// import { strict as assert } from 'node:assert'; +// import testUtils, { GLOBAL } from '../test-utils'; +// import { transformArguments } from './COMMAND_GETKEYSANDFLAGS'; -describe('COMMAND GETKEYSANDFLAGS', () => { - testUtils.isVersionGreaterThanHook([7]); +// describe('COMMAND GETKEYSANDFLAGS', () => { +// testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(['GET', 'key']), - ['COMMAND', 'GETKEYSANDFLAGS', 'GET', 'key'] - ); - }); +// it('transformArguments', () => { +// assert.deepEqual( +// transformArguments(['GET', 'key']), +// ['COMMAND', 'GETKEYSANDFLAGS', 'GET', 'key'] +// ); +// }); - testUtils.testWithClient('client.commandGetKeysAndFlags', async client => { - assert.deepEqual( - await client.commandGetKeysAndFlags(['GET', 'key']), - [{ - key: 'key', - flags: ['RO', 'access'] - }] - ); - }, GLOBAL.SERVERS.OPEN); -}); +// testUtils.testWithClient('client.commandGetKeysAndFlags', async client => { +// assert.deepEqual( +// await client.commandGetKeysAndFlags(['GET', 'key']), +// [{ +// key: 'key', +// flags: ['RO', 'access'] +// }] +// ); +// }, GLOBAL.SERVERS.OPEN); +// }); diff --git a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts index 96b28186ccd..a032190c16e 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts @@ -1,24 +1,23 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, SetReply, UnwrapReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; +export type CommandGetKeysAndFlagsRawReply = ArrayReply +]>>; -export function transformArguments(args: Array): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(args: Array) { return ['COMMAND', 'GETKEYSANDFLAGS', ...args]; -} - -type KeysAndFlagsRawReply = Array<[ - RedisCommandArgument, - RedisCommandArguments -]>; - -type KeysAndFlagsReply = Array<{ - key: RedisCommandArgument; - flags: RedisCommandArguments; -}>; - -export function transformReply(reply: KeysAndFlagsRawReply): KeysAndFlagsReply { - return reply.map(([key, flags]) => ({ + }, + transformReply(reply: UnwrapReply) { + return reply.map(entry => { + const [key, flags] = entry as unknown as UnwrapReply; + return { key, flags - })); -} + }; + }); + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/COMMAND_INFO.spec.ts b/packages/client/lib/commands/COMMAND_INFO.spec.ts index c54a5d0aeb3..fd8c22ae803 100644 --- a/packages/client/lib/commands/COMMAND_INFO.spec.ts +++ b/packages/client/lib/commands/COMMAND_INFO.spec.ts @@ -1,49 +1,49 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './COMMAND_INFO'; -import { CommandCategories, CommandFlags, CommandReply } from './generic-transformers'; +// import { strict as assert } from 'node:assert'; +// import testUtils, { GLOBAL } from '../test-utils'; +// import { transformArguments } from './COMMAND_INFO'; +// import { CommandCategories, CommandFlags, CommandReply } from './generic-transformers'; -export function assertPingCommand(commandInfo: CommandReply | null | undefined): void { - assert.deepEqual( - commandInfo, - { - name: 'ping', - arity: -1, - flags: new Set( - testUtils.isVersionGreaterThan([7]) ? - [CommandFlags.FAST] : - [CommandFlags.STALE, CommandFlags.FAST] - ), - firstKeyIndex: 0, - lastKeyIndex: 0, - step: 0, - categories: new Set( - testUtils.isVersionGreaterThan([6]) ? - [CommandCategories.FAST, CommandCategories.CONNECTION] : - [] - ) - } - ); -} +// export function assertPingCommand(commandInfo: CommandReply | null | undefined): void { +// assert.deepEqual( +// commandInfo, +// { +// name: 'ping', +// arity: -1, +// flags: new Set( +// testUtils.isVersionGreaterThan([7]) ? +// [CommandFlags.FAST] : +// [CommandFlags.STALE, CommandFlags.FAST] +// ), +// firstKeyIndex: 0, +// lastKeyIndex: 0, +// step: 0, +// categories: new Set( +// testUtils.isVersionGreaterThan([6]) ? +// [CommandCategories.FAST, CommandCategories.CONNECTION] : +// [] +// ) +// } +// ); +// } -describe('COMMAND INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(['PING']), - ['COMMAND', 'INFO', 'PING'] - ); - }); +// describe('COMMAND INFO', () => { +// it('transformArguments', () => { +// assert.deepEqual( +// transformArguments(['PING']), +// ['COMMAND', 'INFO', 'PING'] +// ); +// }); - describe('client.commandInfo', () => { - testUtils.testWithClient('PING', async client => { - assertPingCommand((await client.commandInfo(['PING']))[0]); - }, GLOBAL.SERVERS.OPEN); +// describe('client.commandInfo', () => { +// testUtils.testWithClient('PING', async client => { +// assertPingCommand((await client.commandInfo(['PING']))[0]); +// }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('DOSE_NOT_EXISTS', async client => { - assert.deepEqual( - await client.commandInfo(['DOSE_NOT_EXISTS']), - [null] - ); - }, GLOBAL.SERVERS.OPEN); - }); -}); +// testUtils.testWithClient('DOSE_NOT_EXISTS', async client => { +// assert.deepEqual( +// await client.commandInfo(['DOSE_NOT_EXISTS']), +// [null] +// ); +// }, GLOBAL.SERVERS.OPEN); +// }); +// }); diff --git a/packages/client/lib/commands/COMMAND_INFO.ts b/packages/client/lib/commands/COMMAND_INFO.ts index 6f84d0edaf9..5dbd00e8056 100644 --- a/packages/client/lib/commands/COMMAND_INFO.ts +++ b/packages/client/lib/commands/COMMAND_INFO.ts @@ -1,12 +1,14 @@ -import { RedisCommandArguments } from '.'; +import { ArrayReply, Command, UnwrapReply } from '../RESP/types'; import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments(commands: Array): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(commands: Array) { return ['COMMAND', 'INFO', ...commands]; -} - -export function transformReply(reply: Array): Array { + }, + // TODO: This works, as we don't currently handle any of the items returned as a map + transformReply(reply: UnwrapReply>): Array { return reply.map(command => command ? transformCommandReply(command) : null); -} + } +} as const satisfies Command; \ No newline at end of file diff --git a/packages/client/lib/commands/COMMAND_LIST.spec.ts b/packages/client/lib/commands/COMMAND_LIST.spec.ts index eef747d9378..28a9a203bc8 100644 --- a/packages/client/lib/commands/COMMAND_LIST.spec.ts +++ b/packages/client/lib/commands/COMMAND_LIST.spec.ts @@ -1,56 +1,62 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, FilterBy } from './COMMAND_LIST'; +import COMMAND_LIST from './COMMAND_LIST'; describe('COMMAND LIST', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['COMMAND', 'LIST'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + COMMAND_LIST.transformArguments(), + ['COMMAND', 'LIST'] + ); + }); - describe('with FILTERBY', () => { - it('MODULE', () => { - assert.deepEqual( - transformArguments({ - filterBy: FilterBy.MODULE, - value: 'json' - }), - ['COMMAND', 'LIST', 'FILTERBY', 'MODULE', 'json'] - ); - }); + describe('with FILTERBY', () => { + it('MODULE', () => { + assert.deepEqual( + COMMAND_LIST.transformArguments({ + FILTERBY: { + type: 'MODULE', + value: 'JSON' + } + }), + ['COMMAND', 'LIST', 'FILTERBY', 'MODULE', 'JSON'] + ); + }); - it('ACLCAT', () => { - assert.deepEqual( - transformArguments({ - filterBy: FilterBy.ACLCAT, - value: 'admin' - }), - ['COMMAND', 'LIST', 'FILTERBY', 'ACLCAT', 'admin'] - ); - }); + it('ACLCAT', () => { + assert.deepEqual( + COMMAND_LIST.transformArguments({ + FILTERBY: { + type: 'ACLCAT', + value: 'admin' + } + }), + ['COMMAND', 'LIST', 'FILTERBY', 'ACLCAT', 'admin'] + ); + }); - it('PATTERN', () => { - assert.deepEqual( - transformArguments({ - filterBy: FilterBy.PATTERN, - value: 'a*' - }), - ['COMMAND', 'LIST', 'FILTERBY', 'PATTERN', 'a*'] - ); - }); - }); + it('PATTERN', () => { + assert.deepEqual( + COMMAND_LIST.transformArguments({ + FILTERBY: { + type: 'PATTERN', + value: 'a*' + } + }), + ['COMMAND', 'LIST', 'FILTERBY', 'PATTERN', 'a*'] + ); + }); }); + }); - testUtils.testWithClient('client.commandList', async client => { - const commandList = await client.commandList(); - assert.ok(Array.isArray(commandList)); - for (const command of commandList) { - assert.ok(typeof command === 'string'); - } - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.commandList', async client => { + const commandList = await client.commandList(); + assert.ok(Array.isArray(commandList)); + for (const command of commandList) { + assert.ok(typeof command === 'string'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/COMMAND_LIST.ts b/packages/client/lib/commands/COMMAND_LIST.ts index a197bd1a4c6..e73cfdc1a0b 100644 --- a/packages/client/lib/commands/COMMAND_LIST.ts +++ b/packages/client/lib/commands/COMMAND_LIST.ts @@ -1,31 +1,35 @@ -import { RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; +export const COMMAND_LIST_FILTER_BY = { + MODULE: 'MODULE', + ACLCAT: 'ACLCAT', + PATTERN: 'PATTERN' +} as const; -export enum FilterBy { - MODULE = 'MODULE', - ACLCAT = 'ACLCAT', - PATTERN = 'PATTERN' -} +export type CommandListFilterBy = typeof COMMAND_LIST_FILTER_BY[keyof typeof COMMAND_LIST_FILTER_BY]; -interface Filter { - filterBy: FilterBy; - value: string; +export interface CommandListOptions { + FILTERBY?: { + type: CommandListFilterBy; + value: RedisArgument; + }; } - -export function transformArguments(filter?: Filter): RedisCommandArguments { - const args = ['COMMAND', 'LIST']; - - if (filter) { - args.push( - 'FILTERBY', - filter.filterBy, - filter.value - ); +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(options?: CommandListOptions) { + const args: Array = ['COMMAND', 'LIST']; + + if (options?.FILTERBY) { + args.push( + 'FILTERBY', + options.FILTERBY.type, + options.FILTERBY.value + ); } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CONFIG_GET.spec.ts b/packages/client/lib/commands/CONFIG_GET.spec.ts index 83b5c410cfb..94bb2fadcb9 100644 --- a/packages/client/lib/commands/CONFIG_GET.spec.ts +++ b/packages/client/lib/commands/CONFIG_GET.spec.ts @@ -1,11 +1,31 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CONFIG_GET'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import CONFIG_GET from './CONFIG_GET'; describe('CONFIG GET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('*'), - ['CONFIG', 'GET', '*'] - ); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + CONFIG_GET.transformArguments('*'), + ['CONFIG', 'GET', '*'] + ); }); + + it('Array', () => { + assert.deepEqual( + CONFIG_GET.transformArguments(['1', '2']), + ['CONFIG', 'GET', '1', '2'] + ); + }); + }); + + + testUtils.testWithClient('client.configGet', async client => { + const config = await client.configGet('*'); + assert.equal(typeof config, 'object'); + for (const [key, value] of Object.entries(config)) { + assert.equal(typeof key, 'string'); + assert.equal(typeof value, 'string'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CONFIG_GET.ts b/packages/client/lib/commands/CONFIG_GET.ts index 3afc0eddfd0..72fb6e56f5f 100644 --- a/packages/client/lib/commands/CONFIG_GET.ts +++ b/packages/client/lib/commands/CONFIG_GET.ts @@ -1,5 +1,14 @@ -export function transformArguments(parameter: string): Array { - return ['CONFIG', 'GET', parameter]; -} +import { MapReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments, transformTuplesReply } from './generic-transformers'; -export { transformTuplesReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(parameters: RedisVariadicArgument) { + return pushVariadicArguments(['CONFIG', 'GET'], parameters); + }, + transformReply: { + 2: transformTuplesReply, + 3: undefined as unknown as () => MapReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts b/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts index d3f3048b944..c0699e182fc 100644 --- a/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts +++ b/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CONFIG_RESETSTAT'; +import { strict as assert } from 'node:assert'; +import CONFIG_RESETSTAT from './CONFIG_RESETSTAT'; describe('CONFIG RESETSTAT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CONFIG', 'RESETSTAT'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CONFIG_RESETSTAT.transformArguments(), + ['CONFIG', 'RESETSTAT'] + ); + }); }); diff --git a/packages/client/lib/commands/CONFIG_RESETSTAT.ts b/packages/client/lib/commands/CONFIG_RESETSTAT.ts index aba54bc3c7b..4d5deb18b47 100644 --- a/packages/client/lib/commands/CONFIG_RESETSTAT.ts +++ b/packages/client/lib/commands/CONFIG_RESETSTAT.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['CONFIG', 'RESETSTAT']; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CONFIG', 'RESETSTAT']; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CONFIG_REWRITE.spec.ts b/packages/client/lib/commands/CONFIG_REWRITE.spec.ts index cbc3e5b59d8..d612ae216bc 100644 --- a/packages/client/lib/commands/CONFIG_REWRITE.spec.ts +++ b/packages/client/lib/commands/CONFIG_REWRITE.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CONFIG_REWRITE'; +import { strict as assert } from 'node:assert'; +import CONFIG_REWRITE from './CONFIG_REWRITE'; describe('CONFIG REWRITE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['CONFIG', 'REWRITE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + CONFIG_REWRITE.transformArguments(), + ['CONFIG', 'REWRITE'] + ); + }); }); diff --git a/packages/client/lib/commands/CONFIG_REWRITE.ts b/packages/client/lib/commands/CONFIG_REWRITE.ts index 67984adf300..6fbc4b1fa27 100644 --- a/packages/client/lib/commands/CONFIG_REWRITE.ts +++ b/packages/client/lib/commands/CONFIG_REWRITE.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['CONFIG', 'REWRITE']; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['CONFIG', 'REWRITE']; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/CONFIG_SET.spec.ts b/packages/client/lib/commands/CONFIG_SET.spec.ts index 93a7a6ff25e..060183f58d1 100644 --- a/packages/client/lib/commands/CONFIG_SET.spec.ts +++ b/packages/client/lib/commands/CONFIG_SET.spec.ts @@ -1,24 +1,32 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './CONFIG_SET'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import CONFIG_SET from './CONFIG_SET'; describe('CONFIG SET', () => { - describe('transformArguments', () => { - it('set one parameter (old version)', () => { - assert.deepEqual( - transformArguments('parameter', 'value'), - ['CONFIG', 'SET', 'parameter', 'value'] - ); - }); + describe('transformArguments', () => { + it('set one parameter (old version)', () => { + assert.deepEqual( + CONFIG_SET.transformArguments('parameter', 'value'), + ['CONFIG', 'SET', 'parameter', 'value'] + ); + }); - it('set muiltiple parameters', () => { - assert.deepEqual( - transformArguments({ - 1: 'a', - 2: 'b', - 3: 'c' - }), - ['CONFIG', 'SET', '1', 'a', '2', 'b', '3', 'c'] - ); - }); + it('set muiltiple parameters', () => { + assert.deepEqual( + CONFIG_SET.transformArguments({ + 1: 'a', + 2: 'b', + 3: 'c' + }), + ['CONFIG', 'SET', '1', 'a', '2', 'b', '3', 'c'] + ); }); + }); + + testUtils.testWithClient('client.configSet', async client => { + assert.equal( + await client.configSet('maxmemory', '0'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/CONFIG_SET.ts b/packages/client/lib/commands/CONFIG_SET.ts index 41f40d035d2..c7072245e22 100644 --- a/packages/client/lib/commands/CONFIG_SET.ts +++ b/packages/client/lib/commands/CONFIG_SET.ts @@ -1,23 +1,26 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { SimpleStringReply, Command, RedisArgument } from '../RESP/types'; -type SingleParameter = [parameter: RedisCommandArgument, value: RedisCommandArgument]; +type SingleParameter = [parameter: RedisArgument, value: RedisArgument]; -type MultipleParameters = [config: Record]; +type MultipleParameters = [config: Record]; -export function transformArguments( +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments( ...[parameterOrConfig, value]: SingleParameter | MultipleParameters -): RedisCommandArguments { - const args: RedisCommandArguments = ['CONFIG', 'SET']; - - if (typeof parameterOrConfig === 'string') { - args.push(parameterOrConfig, value!); + ) { + const args: Array = ['CONFIG', 'SET']; + + if (typeof parameterOrConfig === 'string' || parameterOrConfig instanceof Buffer) { + args.push(parameterOrConfig, value!); } else { - for (const [key, value] of Object.entries(parameterOrConfig)) { - args.push(key, value); - } + for (const [key, value] of Object.entries(parameterOrConfig)) { + args.push(key, value); + } } - + return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/COPY.spec.ts b/packages/client/lib/commands/COPY.spec.ts index 0d68e969cdb..c4c26c30dc2 100644 --- a/packages/client/lib/commands/COPY.spec.ts +++ b/packages/client/lib/commands/COPY.spec.ts @@ -1,67 +1,54 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './COPY'; +import COPY from './COPY'; describe('COPY', () => { - testUtils.isVersionGreaterThanHook([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'] - ); - }); + testUtils.isVersionGreaterThanHook([6, 2]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + COPY.transformArguments('source', 'destination'), + ['COPY', 'source', 'destination'] + ); }); - describe('transformReply', () => { - it('0', () => { - assert.equal( - transformReply(0), - false - ); - }); + it('with destination DB flag', () => { + assert.deepEqual( + COPY.transformArguments('source', 'destination', { + DB: 1 + }), + ['COPY', 'source', 'destination', 'DB', '1'] + ); + }); - it('1', () => { - assert.equal( - transformReply(1), - true - ); - }); + it('with replace flag', () => { + assert.deepEqual( + COPY.transformArguments('source', 'destination', { + REPLACE: true + }), + ['COPY', 'source', 'destination', 'REPLACE'] + ); }); - testUtils.testWithClient('client.copy', async client => { - assert.equal( - await client.copy('source', 'destination'), - false - ); - }, GLOBAL.SERVERS.OPEN); + it('with both flags', () => { + assert.deepEqual( + COPY.transformArguments('source', 'destination', { + DB: 1, + REPLACE: true + }), + ['COPY', 'source', 'destination', 'DB', '1', 'REPLACE'] + ); + }); + }); + + testUtils.testAll('copy', async client => { + assert.equal( + await client.copy('{tag}source', '{tag}destination'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/COPY.ts b/packages/client/lib/commands/COPY.ts index b1e212a9956..a65948cf944 100644 --- a/packages/client/lib/commands/COPY.ts +++ b/packages/client/lib/commands/COPY.ts @@ -1,28 +1,25 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -interface CopyCommandOptions { - destinationDb?: number; - replace?: boolean; +export interface CopyCommandOptions { + DB?: number; + REPLACE?: boolean; } -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument, - options?: CopyCommandOptions -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(source: RedisArgument, destination: RedisArgument, options?: CopyCommandOptions) { const args = ['COPY', source, destination]; - if (options?.destinationDb) { - args.push('DB', options.destinationDb.toString()); + if (options?.DB) { + args.push('DB', options.DB.toString()); } - if (options?.replace) { - args.push('REPLACE'); + if (options?.REPLACE) { + args.push('REPLACE'); } return args; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/DBSIZE.spec.ts b/packages/client/lib/commands/DBSIZE.spec.ts index a014a46e6e2..bd668d166e7 100644 --- a/packages/client/lib/commands/DBSIZE.spec.ts +++ b/packages/client/lib/commands/DBSIZE.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DBSIZE'; +import DBSIZE from './DBSIZE'; describe('DBSIZE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['DBSIZE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + DBSIZE.transformArguments(), + ['DBSIZE'] + ); + }); - testUtils.testWithClient('client.dbSize', async client => { - assert.equal( - await client.dbSize(), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.dbSize', async client => { + assert.equal( + await client.dbSize(), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/DBSIZE.ts b/packages/client/lib/commands/DBSIZE.ts index 6b442ec33a2..54770831ab0 100644 --- a/packages/client/lib/commands/DBSIZE.ts +++ b/packages/client/lib/commands/DBSIZE.ts @@ -1,7 +1,10 @@ -export const IS_READ_ONLY = true; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['DBSIZE']; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/DECR.spec.ts b/packages/client/lib/commands/DECR.spec.ts index 75e1205feda..80d6c8eb55e 100644 --- a/packages/client/lib/commands/DECR.spec.ts +++ b/packages/client/lib/commands/DECR.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DECR'; +import DECR from './DECR'; describe('DECR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['DECR', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + DECR.transformArguments('key'), + ['DECR', 'key'] + ); + }); - testUtils.testWithClient('client.decr', async client => { - assert.equal( - await client.decr('key'), - -1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('decr', async client => { + assert.equal( + await client.decr('key'), + -1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/DECR.ts b/packages/client/lib/commands/DECR.ts index 2b5f2c4bb5c..540148b7eed 100644 --- a/packages/client/lib/commands/DECR.ts +++ b/packages/client/lib/commands/DECR.ts @@ -1,9 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument) { return ['DECR', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/DECRBY.spec.ts b/packages/client/lib/commands/DECRBY.spec.ts index d2c23e94728..fc0c1033187 100644 --- a/packages/client/lib/commands/DECRBY.spec.ts +++ b/packages/client/lib/commands/DECRBY.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DECRBY'; +import DECRBY from './DECRBY'; describe('DECRBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 2), - ['DECRBY', 'key', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + DECRBY.transformArguments('key', 2), + ['DECRBY', 'key', '2'] + ); + }); - testUtils.testWithClient('client.decrBy', async client => { - assert.equal( - await client.decrBy('key', 2), - -2 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('decrBy', async client => { + assert.equal( + await client.decrBy('key', 2), + -2 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/DECRBY.ts b/packages/client/lib/commands/DECRBY.ts index afe4d79f0a1..77d56939dd5 100644 --- a/packages/client/lib/commands/DECRBY.ts +++ b/packages/client/lib/commands/DECRBY.ts @@ -1,12 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - decrement: number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, decrement: number) { return ['DECRBY', key, decrement.toString()]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/DEL.spec.ts b/packages/client/lib/commands/DEL.spec.ts index 75a29a8f641..caac8ac13b5 100644 --- a/packages/client/lib/commands/DEL.spec.ts +++ b/packages/client/lib/commands/DEL.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DEL'; +import DEL from './DEL'; describe('DEL', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['DEL', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + DEL.transformArguments('key'), + ['DEL', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['key1', 'key2']), - ['DEL', 'key1', 'key2'] - ); - }); + it('array', () => { + assert.deepEqual( + DEL.transformArguments(['key1', 'key2']), + ['DEL', 'key1', 'key2'] + ); }); + }); - testUtils.testWithClient('client.del', async client => { - assert.equal( - await client.del('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('del', async client => { + assert.equal( + await client.del('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/DEL.ts b/packages/client/lib/commands/DEL.ts index d60abe0f28e..f59a5ba2e89 100644 --- a/packages/client/lib/commands/DEL.ts +++ b/packages/client/lib/commands/DEL.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - keys: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['DEL'], keys); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(keys: RedisVariadicArgument) { + return pushVariadicArguments(['DEL'], keys); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/DISCARD.spec.ts b/packages/client/lib/commands/DISCARD.spec.ts index b01f9d650d9..76e0abd57af 100644 --- a/packages/client/lib/commands/DISCARD.spec.ts +++ b/packages/client/lib/commands/DISCARD.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './DISCARD'; +import { strict as assert } from 'node:assert'; +import DISCARD from './DISCARD'; describe('DISCARD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['DISCARD'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + DISCARD.transformArguments(), + ['DISCARD'] + ); + }); }); diff --git a/packages/client/lib/commands/DISCARD.ts b/packages/client/lib/commands/DISCARD.ts index acad8a722e1..e153070c989 100644 --- a/packages/client/lib/commands/DISCARD.ts +++ b/packages/client/lib/commands/DISCARD.ts @@ -1,7 +1,8 @@ -import { RedisCommandArgument } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): Array { +export default { + transformArguments() { return ['DISCARD']; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/DUMP.spec.ts b/packages/client/lib/commands/DUMP.spec.ts index aebbf4f3f7c..15be3fae086 100644 --- a/packages/client/lib/commands/DUMP.spec.ts +++ b/packages/client/lib/commands/DUMP.spec.ts @@ -1,11 +1,14 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; describe('DUMP', () => { - testUtils.testWithClient('client.dump', async client => { - assert.equal( - await client.dump('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('client.dump', async client => { + assert.equal( + await client.dump('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/DUMP.ts b/packages/client/lib/commands/DUMP.ts index fd4354db45c..06b12f06d09 100644 --- a/packages/client/lib/commands/DUMP.ts +++ b/packages/client/lib/commands/DUMP.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['DUMP', key]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ECHO.spec.ts b/packages/client/lib/commands/ECHO.spec.ts index 27f6b2a17d3..c0d0725282c 100644 --- a/packages/client/lib/commands/ECHO.spec.ts +++ b/packages/client/lib/commands/ECHO.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ECHO'; +import ECHO from './ECHO'; describe('ECHO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('message'), - ['ECHO', 'message'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ECHO.transformArguments('message'), + ['ECHO', 'message'] + ); + }); - testUtils.testWithClient('client.echo', async client => { - assert.equal( - await client.echo('message'), - 'message' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.echo', async client => { + assert.equal( + await client.echo('message'), + 'message' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ECHO.ts b/packages/client/lib/commands/ECHO.ts index 7a837307e2b..dfe5ec13000 100644 --- a/packages/client/lib/commands/ECHO.ts +++ b/packages/client/lib/commands/ECHO.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(message: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(message: RedisArgument) { return ['ECHO', message]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/EVAL.spec.ts b/packages/client/lib/commands/EVAL.spec.ts index 7aa029362fd..2aea64e0991 100644 --- a/packages/client/lib/commands/EVAL.spec.ts +++ b/packages/client/lib/commands/EVAL.spec.ts @@ -1,29 +1,25 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './EVAL'; +import EVAL 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'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + EVAL.transformArguments('return KEYS[1] + ARGV[1]', { + keys: ['key'], + arguments: ['argument'] + }), + ['EVAL', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument'] + ); + }); - testUtils.testWithClient('client.eval', async client => { - assert.equal( - await client.eval('return 1'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.eval', async cluster => { - assert.equal( - await cluster.eval('return 1'), - 1 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('eval', async client => { + assert.equal( + await client.eval('return 1'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/EVAL.ts b/packages/client/lib/commands/EVAL.ts index a82f8bf0aad..21684e7a313 100644 --- a/packages/client/lib/commands/EVAL.ts +++ b/packages/client/lib/commands/EVAL.ts @@ -1,7 +1,33 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { RedisArgument, ReplyUnion, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = evalFirstKeyIndex; +export interface EvalOptions { + keys?: Array; + arguments?: Array; +} + +export function transformEvalArguments( + command: RedisArgument, + script: RedisArgument, + options?: EvalOptions +) { + const args = [command, script]; + + if (options?.keys) { + args.push(options.keys.length.toString(), ...options.keys); + } else { + args.push('0'); + } -export function transformArguments(script: string, options?: EvalOptions): Array { - return pushEvalArguments(['EVAL', script], options); + if (options?.arguments) { + args.push(...options.arguments); + } + + return args; } + +export default { + FIRST_KEY_INDEX: (_, options?: EvalOptions) => options?.keys?.[0], + IS_READ_ONLY: false, + transformArguments: transformEvalArguments.bind(undefined, 'EVAL'), + transformReply: undefined as unknown as () => ReplyUnion +} as const satisfies Command; diff --git a/packages/client/lib/commands/EVALSHA.spec.ts b/packages/client/lib/commands/EVALSHA.spec.ts index 08b330ac4f5..81d3a0ec2b0 100644 --- a/packages/client/lib/commands/EVALSHA.spec.ts +++ b/packages/client/lib/commands/EVALSHA.spec.ts @@ -1,14 +1,14 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './EVALSHA'; +import { strict as assert } from 'node:assert'; +import EVALSHA from './EVALSHA'; describe('EVALSHA', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('sha1', { - keys: ['key'], - arguments: ['argument'] - }), - ['EVALSHA', 'sha1', '1', 'key', 'argument'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + EVALSHA.transformArguments('sha1', { + keys: ['key'], + arguments: ['argument'] + }), + ['EVALSHA', 'sha1', '1', 'key', 'argument'] + ); + }); }); diff --git a/packages/client/lib/commands/EVALSHA.ts b/packages/client/lib/commands/EVALSHA.ts index 24f7060a052..dc4127f90da 100644 --- a/packages/client/lib/commands/EVALSHA.ts +++ b/packages/client/lib/commands/EVALSHA.ts @@ -1,7 +1,9 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { transformEvalArguments } from './EVAL'; -export const FIRST_KEY_INDEX = evalFirstKeyIndex; - -export function transformArguments(sha1: string, options?: EvalOptions): Array { - return pushEvalArguments(['EVALSHA', sha1], options); -} +export default { + FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, + IS_READ_ONLY: false, + transformArguments: transformEvalArguments.bind(undefined, 'EVALSHA'), + transformReply: EVAL.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/EVALSHA_RO.spec.ts b/packages/client/lib/commands/EVALSHA_RO.spec.ts index 939a4a209cb..20b4a27e0d1 100644 --- a/packages/client/lib/commands/EVALSHA_RO.spec.ts +++ b/packages/client/lib/commands/EVALSHA_RO.spec.ts @@ -1,17 +1,17 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; -import { transformArguments } from './EVALSHA_RO'; +import EVALSHA_RO from './EVALSHA_RO'; describe('EVALSHA_RO', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('sha1', { - keys: ['key'], - arguments: ['argument'] - }), - ['EVALSHA_RO', 'sha1', '1', 'key', 'argument'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + EVALSHA_RO.transformArguments('sha1', { + keys: ['key'], + arguments: ['argument'] + }), + ['EVALSHA_RO', 'sha1', '1', 'key', 'argument'] + ); + }); }); diff --git a/packages/client/lib/commands/EVALSHA_RO.ts b/packages/client/lib/commands/EVALSHA_RO.ts index c3fcc3dc9cf..fe9042bd5fd 100644 --- a/packages/client/lib/commands/EVALSHA_RO.ts +++ b/packages/client/lib/commands/EVALSHA_RO.ts @@ -1,9 +1,9 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { transformEvalArguments } from './EVAL'; -export const FIRST_KEY_INDEX = evalFirstKeyIndex; - -export const IS_READ_ONLY = true; - -export function transformArguments(sha1: string, options?: EvalOptions): Array { - return pushEvalArguments(['EVALSHA_RO', sha1], options); -} +export default { + FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, + IS_READ_ONLY: true, + transformArguments: transformEvalArguments.bind(undefined, 'EVALSHA_RO'), + transformReply: EVAL.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/EVAL_RO.spec.ts b/packages/client/lib/commands/EVAL_RO.spec.ts index f71d0b2b24a..3f071e80681 100644 --- a/packages/client/lib/commands/EVAL_RO.spec.ts +++ b/packages/client/lib/commands/EVAL_RO.spec.ts @@ -1,31 +1,27 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './EVAL_RO'; +import EVAL_RO from './EVAL_RO'; describe('EVAL_RO', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('return KEYS[1] + ARGV[1]', { - keys: ['key'], - arguments: ['argument'] - }), - ['EVAL_RO', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + EVAL_RO.transformArguments('return KEYS[1] + ARGV[1]', { + keys: ['key'], + arguments: ['argument'] + }), + ['EVAL_RO', 'return KEYS[1] + ARGV[1]', '1', 'key', 'argument'] + ); + }); - testUtils.testWithClient('client.evalRo', async client => { - assert.equal( - await client.evalRo('return 1'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.evalRo', async cluster => { - assert.equal( - await cluster.evalRo('return 1'), - 1 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('evalRo', async cluster => { + assert.equal( + await cluster.evalRo('return 1'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/EVAL_RO.ts b/packages/client/lib/commands/EVAL_RO.ts index 590c3af04f3..2608e28f789 100644 --- a/packages/client/lib/commands/EVAL_RO.ts +++ b/packages/client/lib/commands/EVAL_RO.ts @@ -1,9 +1,9 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { transformEvalArguments } from './EVAL'; -export const FIRST_KEY_INDEX = evalFirstKeyIndex; - -export const IS_READ_ONLY = true; - -export function transformArguments(script: string, options?: EvalOptions): Array { - return pushEvalArguments(['EVAL_RO', script], options); -} +export default { + FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, + IS_READ_ONLY: true, + transformArguments: transformEvalArguments.bind(undefined, 'EVAL_RO'), + transformReply: EVAL.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/EXISTS.spec.ts b/packages/client/lib/commands/EXISTS.spec.ts index be1a808225e..695795697f1 100644 --- a/packages/client/lib/commands/EXISTS.spec.ts +++ b/packages/client/lib/commands/EXISTS.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './EXISTS'; +import EXISTS from './EXISTS'; describe('EXISTS', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['EXISTS', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + EXISTS.transformArguments('key'), + ['EXISTS', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['EXISTS', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + EXISTS.transformArguments(['1', '2']), + ['EXISTS', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.exists', async client => { - assert.equal( - await client.exists('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('exists', async client => { + assert.equal( + await client.exists('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/EXISTS.ts b/packages/client/lib/commands/EXISTS.ts index 3bbc72ada46..a077943b8d2 100644 --- a/packages/client/lib/commands/EXISTS.ts +++ b/packages/client/lib/commands/EXISTS.ts @@ -1,14 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - keys: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['EXISTS'], keys); -} - -export declare function transformReply(): number; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(keys: RedisVariadicArgument) { + return pushVariadicArguments(['EXISTS'], keys); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/EXPIRE.spec.ts b/packages/client/lib/commands/EXPIRE.spec.ts index 39f9d70bd93..817e37cca45 100644 --- a/packages/client/lib/commands/EXPIRE.spec.ts +++ b/packages/client/lib/commands/EXPIRE.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './EXPIRE'; +import EXPIRE from './EXPIRE'; describe('EXPIRE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 1), - ['EXPIRE', 'key', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + EXPIRE.transformArguments('key', 1), + ['EXPIRE', 'key', '1'] + ); + }); - it('with set option', () => { - assert.deepEqual( - transformArguments('key', 1, 'NX'), - ['EXPIRE', 'key', '1', 'NX'] - ); - }); + it('with set option', () => { + assert.deepEqual( + EXPIRE.transformArguments('key', 1, 'NX'), + ['EXPIRE', 'key', '1', 'NX'] + ); }); + }); - testUtils.testWithClient('client.expire', async client => { - assert.equal( - await client.expire('key', 0), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('expire', async client => { + assert.equal( + await client.expire('key', 0), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/EXPIRE.ts b/packages/client/lib/commands/EXPIRE.ts index d9e85595c3b..3e57769bd6e 100644 --- a/packages/client/lib/commands/EXPIRE.ts +++ b/packages/client/lib/commands/EXPIRE.ts @@ -1,19 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, seconds: number, mode?: 'NX' | 'XX' | 'GT' | 'LT' -): RedisCommandArguments { + ) { const args = ['EXPIRE', key, seconds.toString()]; if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/EXPIREAT.spec.ts b/packages/client/lib/commands/EXPIREAT.spec.ts index 0335b36f5f5..31efdcea398 100644 --- a/packages/client/lib/commands/EXPIREAT.spec.ts +++ b/packages/client/lib/commands/EXPIREAT.spec.ts @@ -1,36 +1,39 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './EXPIREAT'; +import EXPIREAT from './EXPIREAT'; describe('EXPIREAT', () => { - describe('transformArguments', () => { - it('number', () => { - assert.deepEqual( - transformArguments('key', 1), - ['EXPIREAT', 'key', '1'] - ); - }); + describe('transformArguments', () => { + it('number', () => { + assert.deepEqual( + EXPIREAT.transformArguments('key', 1), + ['EXPIREAT', 'key', '1'] + ); + }); + + it('date', () => { + const d = new Date(); + assert.deepEqual( + EXPIREAT.transformArguments('key', d), + ['EXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString()] + ); + }); - it('date', () => { - const d = new Date(); - assert.deepEqual( - transformArguments('key', d), - ['EXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString()] - ); - }); - - it('with set option', () => { - assert.deepEqual( - transformArguments('key', 1, 'GT'), - ['EXPIREAT', 'key', '1', 'GT'] - ); - }); + it('with set option', () => { + assert.deepEqual( + EXPIREAT.transformArguments('key', 1, 'GT'), + ['EXPIREAT', 'key', '1', 'GT'] + ); }); + }); - testUtils.testWithClient('client.expireAt', async client => { - assert.equal( - await client.expireAt('key', 1), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('expireAt', async client => { + assert.equal( + await client.expireAt('key', 1), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/EXPIREAT.ts b/packages/client/lib/commands/EXPIREAT.ts index 84e7760fe7f..9a959a87f99 100644 --- a/packages/client/lib/commands/EXPIREAT.ts +++ b/packages/client/lib/commands/EXPIREAT.ts @@ -1,24 +1,21 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformEXAT } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, timestamp: number | Date, mode?: 'NX' | 'XX' | 'GT' | 'LT' -): RedisCommandArguments { - const args = [ - 'EXPIREAT', - key, - transformEXAT(timestamp) - ]; + ) { + const args = ['EXPIREAT', key, transformEXAT(timestamp)]; if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/EXPIRETIME.spec.ts b/packages/client/lib/commands/EXPIRETIME.spec.ts index 1d63e759a5d..3c202d2427f 100644 --- a/packages/client/lib/commands/EXPIRETIME.spec.ts +++ b/packages/client/lib/commands/EXPIRETIME.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './EXPIRETIME'; +import EXPIRETIME from './EXPIRETIME'; describe('EXPIRETIME', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['EXPIRETIME', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + EXPIRETIME.transformArguments('key'), + ['EXPIRETIME', 'key'] + ); + }); - testUtils.testWithClient('client.expireTime', async client => { - assert.equal( - await client.expireTime('key'), - -2 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('expireTime', async client => { + assert.equal( + await client.expireTime('key'), + -2 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/EXPIRETIME.ts b/packages/client/lib/commands/EXPIRETIME.ts index 8c1bb075995..d6ac35aeb3d 100644 --- a/packages/client/lib/commands/EXPIRETIME.ts +++ b/packages/client/lib/commands/EXPIRETIME.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['EXPIRETIME', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FAILOVER.spec.ts b/packages/client/lib/commands/FAILOVER.spec.ts index 16094a0dbc3..96602caff91 100644 --- a/packages/client/lib/commands/FAILOVER.spec.ts +++ b/packages/client/lib/commands/FAILOVER.spec.ts @@ -1,72 +1,72 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './FAILOVER'; +import { strict as assert } from 'node:assert'; +import FAILOVER from './FAILOVER'; describe('FAILOVER', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['FAILOVER'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + FAILOVER.transformArguments(), + ['FAILOVER'] + ); + }); - describe('with TO', () => { - it('simple', () => { - assert.deepEqual( - transformArguments({ - TO: { - host: 'host', - port: 6379 - } - }), - ['FAILOVER', 'TO', 'host', '6379'] - ); - }); + describe('with TO', () => { + it('simple', () => { + assert.deepEqual( + FAILOVER.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 FORCE', () => { + assert.deepEqual( + FAILOVER.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 ABORT', () => { + assert.deepEqual( + FAILOVER.transformArguments({ + ABORT: true + }), + ['FAILOVER', 'ABORT'] + ); + }); - it('with TIMEOUT', () => { - assert.deepEqual( - transformArguments({ - TIMEOUT: 1 - }), - ['FAILOVER', 'TIMEOUT', '1'] - ); - }); + it('with TIMEOUT', () => { + assert.deepEqual( + FAILOVER.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'] - ); - }); + it('with TO, ABORT, TIMEOUT', () => { + assert.deepEqual( + FAILOVER.transformArguments({ + TO: { + host: 'host', + port: 6379 + }, + ABORT: true, + TIMEOUT: 1 + }), + ['FAILOVER', 'TO', 'host', '6379', 'ABORT', 'TIMEOUT', '1'] + ); }); + }); }); diff --git a/packages/client/lib/commands/FAILOVER.ts b/packages/client/lib/commands/FAILOVER.ts index c31dbc063de..0c1e710d321 100644 --- a/packages/client/lib/commands/FAILOVER.ts +++ b/packages/client/lib/commands/FAILOVER.ts @@ -1,33 +1,36 @@ +import { SimpleStringReply, Command } from '../RESP/types'; + interface FailoverOptions { - TO?: { - host: string; - port: number; - FORCE?: true; - }; - ABORT?: true; - TIMEOUT?: number; + TO?: { + host: string; + port: number; + FORCE?: true; + }; + ABORT?: true; + TIMEOUT?: number; } -export function transformArguments(options?: FailoverOptions): Array { +export default { + transformArguments(options?: FailoverOptions) { const args = ['FAILOVER']; if (options?.TO) { - args.push('TO', options.TO.host, options.TO.port.toString()); + args.push('TO', options.TO.host, options.TO.port.toString()); - if (options.TO.FORCE) { - args.push('FORCE'); - } + if (options.TO.FORCE) { + args.push('FORCE'); + } } if (options?.ABORT) { - args.push('ABORT'); + args.push('ABORT'); } if (options?.TIMEOUT) { - args.push('TIMEOUT', options.TIMEOUT.toString()); + args.push('TIMEOUT', options.TIMEOUT.toString()); } return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FCALL.spec.ts b/packages/client/lib/commands/FCALL.spec.ts index fd29f07527d..286f2a371bf 100644 --- a/packages/client/lib/commands/FCALL.spec.ts +++ b/packages/client/lib/commands/FCALL.spec.ts @@ -1,29 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; -import { transformArguments } from './FCALL'; +import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; +import FCALL from './FCALL'; describe('FCALL', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('function', { - keys: ['key'], - arguments: ['argument'] - }), - ['FCALL', 'function', '1', 'key', 'argument'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + FCALL.transformArguments('function', { + keys: ['key'], + arguments: ['argument'] + }), + ['FCALL', 'function', '1', 'key', 'argument'] + ); + }); - testUtils.testWithClient('client.fCall', async client => { - await loadMathFunction(client); + testUtils.testWithClient('client.fCall', async client => { + const [,, reply] = await Promise.all([ + loadMathFunction(client), + client.set('key', '2'), + client.fCall(MATH_FUNCTION.library.square.NAME, { + keys: ['key'] + }) + ]); - assert.equal( - await client.fCall(MATH_FUNCTION.library.square.NAME, { - arguments: ['2'] - }), - 4 - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 4); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FCALL.ts b/packages/client/lib/commands/FCALL.ts index a4cadedb6f9..ba371a81b13 100644 --- a/packages/client/lib/commands/FCALL.ts +++ b/packages/client/lib/commands/FCALL.ts @@ -1,7 +1,9 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { transformEvalArguments } from './EVAL'; -export const FIRST_KEY_INDEX = evalFirstKeyIndex; - -export function transformArguments(fn: string, options?: EvalOptions): Array { - return pushEvalArguments(['FCALL', fn], options); -} +export default { + FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, + IS_READ_ONLY: false, + transformArguments: transformEvalArguments.bind(undefined, 'FCALL'), + transformReply: EVAL.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FCALL_RO.spec.ts b/packages/client/lib/commands/FCALL_RO.spec.ts index 18665f92aa6..57edcebebef 100644 --- a/packages/client/lib/commands/FCALL_RO.spec.ts +++ b/packages/client/lib/commands/FCALL_RO.spec.ts @@ -1,29 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; -import { transformArguments } from './FCALL_RO'; +import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; +import FCALL_RO from './FCALL_RO'; describe('FCALL_RO', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('function', { - keys: ['key'], - arguments: ['argument'] - }), - ['FCALL_RO', 'function', '1', 'key', 'argument'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + FCALL_RO.transformArguments('function', { + keys: ['key'], + arguments: ['argument'] + }), + ['FCALL_RO', 'function', '1', 'key', 'argument'] + ); + }); - testUtils.testWithClient('client.fCallRo', async client => { - await loadMathFunction(client); + testUtils.testWithClient('client.fCallRo', async client => { + const [,, reply] = await Promise.all([ + loadMathFunction(client), + client.set('key', '2'), + client.fCallRo(MATH_FUNCTION.library.square.NAME, { + keys: ['key'] + }) + ]); - assert.equal( - await client.fCallRo(MATH_FUNCTION.library.square.NAME, { - arguments: ['2'] - }), - 4 - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 4); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FCALL_RO.ts b/packages/client/lib/commands/FCALL_RO.ts index 66b79aa8833..ec002a79f82 100644 --- a/packages/client/lib/commands/FCALL_RO.ts +++ b/packages/client/lib/commands/FCALL_RO.ts @@ -1,9 +1,9 @@ -import { evalFirstKeyIndex, EvalOptions, pushEvalArguments } from './generic-transformers'; +import { Command } from '../RESP/types'; +import EVAL, { transformEvalArguments } from './EVAL'; -export const FIRST_KEY_INDEX = evalFirstKeyIndex; - -export const IS_READ_ONLY = true; - -export function transformArguments(fn: string, options?: EvalOptions): Array { - return pushEvalArguments(['FCALL_RO', fn], options); -} +export default { + FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, + IS_READ_ONLY: false, + transformArguments: transformEvalArguments.bind(undefined, 'FCALL_RO'), + transformReply: EVAL.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FLUSHALL.spec.ts b/packages/client/lib/commands/FLUSHALL.spec.ts index db5bb72e9cb..63ad38dd7de 100644 --- a/packages/client/lib/commands/FLUSHALL.spec.ts +++ b/packages/client/lib/commands/FLUSHALL.spec.ts @@ -1,35 +1,35 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { RedisFlushModes, transformArguments } from './FLUSHALL'; +import FLUSHALL, { REDIS_FLUSH_MODES } from './FLUSHALL'; describe('FLUSHALL', () => { - describe('transformArguments', () => { - it('default', () => { - assert.deepEqual( - transformArguments(), - ['FLUSHALL'] - ); - }); + describe('transformArguments', () => { + it('default', () => { + assert.deepEqual( + FLUSHALL.transformArguments(), + ['FLUSHALL'] + ); + }); - it('ASYNC', () => { - assert.deepEqual( - transformArguments(RedisFlushModes.ASYNC), - ['FLUSHALL', 'ASYNC'] - ); - }); + it('ASYNC', () => { + assert.deepEqual( + FLUSHALL.transformArguments(REDIS_FLUSH_MODES.ASYNC), + ['FLUSHALL', 'ASYNC'] + ); + }); - it('SYNC', () => { - assert.deepEqual( - transformArguments(RedisFlushModes.SYNC), - ['FLUSHALL', 'SYNC'] - ); - }); + it('SYNC', () => { + assert.deepEqual( + FLUSHALL.transformArguments(REDIS_FLUSH_MODES.SYNC), + ['FLUSHALL', 'SYNC'] + ); }); + }); - testUtils.testWithClient('client.flushAll', async client => { - assert.equal( - await client.flushAll(), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.flushAll', async client => { + assert.equal( + await client.flushAll(), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FLUSHALL.ts b/packages/client/lib/commands/FLUSHALL.ts index 967096bb9bd..5e6484a991e 100644 --- a/packages/client/lib/commands/FLUSHALL.ts +++ b/packages/client/lib/commands/FLUSHALL.ts @@ -1,16 +1,23 @@ -export enum RedisFlushModes { - ASYNC = 'ASYNC', - SYNC = 'SYNC' -} +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(mode?: RedisFlushModes): Array { - const args = ['FLUSHALL']; +export const REDIS_FLUSH_MODES = { + ASYNC: 'ASYNC', + SYNC: 'SYNC' +} as const; + +export type RedisFlushMode = typeof REDIS_FLUSH_MODES[keyof typeof REDIS_FLUSH_MODES]; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(mode?: RedisFlushMode) { + const args = ['FLUSHALL']; + if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FLUSHDB.spec.ts b/packages/client/lib/commands/FLUSHDB.spec.ts index bf460e9e7a8..ad09ecfc945 100644 --- a/packages/client/lib/commands/FLUSHDB.spec.ts +++ b/packages/client/lib/commands/FLUSHDB.spec.ts @@ -1,36 +1,36 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { RedisFlushModes } from './FLUSHALL'; -import { transformArguments } from './FLUSHDB'; +import FLUSHDB from './FLUSHDB'; +import { REDIS_FLUSH_MODES } from './FLUSHALL'; describe('FLUSHDB', () => { - describe('transformArguments', () => { - it('default', () => { - assert.deepEqual( - transformArguments(), - ['FLUSHDB'] - ); - }); + describe('transformArguments', () => { + it('default', () => { + assert.deepEqual( + FLUSHDB.transformArguments(), + ['FLUSHDB'] + ); + }); - it('ASYNC', () => { - assert.deepEqual( - transformArguments(RedisFlushModes.ASYNC), - ['FLUSHDB', 'ASYNC'] - ); - }); + it('ASYNC', () => { + assert.deepEqual( + FLUSHDB.transformArguments(REDIS_FLUSH_MODES.ASYNC), + ['FLUSHDB', 'ASYNC'] + ); + }); - it('SYNC', () => { - assert.deepEqual( - transformArguments(RedisFlushModes.SYNC), - ['FLUSHDB', 'SYNC'] - ); - }); + it('SYNC', () => { + assert.deepEqual( + FLUSHDB.transformArguments(REDIS_FLUSH_MODES.SYNC), + ['FLUSHDB', 'SYNC'] + ); }); + }); - testUtils.testWithClient('client.flushDb', async client => { - assert.equal( - await client.flushDb(), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.flushDb', async client => { + assert.equal( + await client.flushDb(), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FLUSHDB.ts b/packages/client/lib/commands/FLUSHDB.ts index 5b8060df9eb..75c7a66f190 100644 --- a/packages/client/lib/commands/FLUSHDB.ts +++ b/packages/client/lib/commands/FLUSHDB.ts @@ -1,13 +1,17 @@ -import { RedisFlushModes } from './FLUSHALL'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { RedisFlushMode } from './FLUSHALL'; -export function transformArguments(mode?: RedisFlushModes): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(mode?: RedisFlushMode) { const args = ['FLUSHDB']; - + if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_DELETE.spec.ts b/packages/client/lib/commands/FUNCTION_DELETE.spec.ts index 563b9aa0a58..1172e84b956 100644 --- a/packages/client/lib/commands/FUNCTION_DELETE.spec.ts +++ b/packages/client/lib/commands/FUNCTION_DELETE.spec.ts @@ -1,24 +1,24 @@ -import { strict as assert } from 'assert'; -import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './FUNCTION_DELETE'; +import FUNCTION_DELETE from './FUNCTION_DELETE'; +import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; describe('FUNCTION DELETE', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('library'), - ['FUNCTION', 'DELETE', 'library'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + FUNCTION_DELETE.transformArguments('library'), + ['FUNCTION', 'DELETE', 'library'] + ); + }); - testUtils.testWithClient('client.functionDelete', async client => { - await loadMathFunction(client); + testUtils.testWithClient('client.functionDelete', async client => { + await loadMathFunction(client); - assert.equal( - await client.functionDelete(MATH_FUNCTION.name), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal( + await client.functionDelete(MATH_FUNCTION.name), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FUNCTION_DELETE.ts b/packages/client/lib/commands/FUNCTION_DELETE.ts index 4aa6be40e12..1061cded17c 100644 --- a/packages/client/lib/commands/FUNCTION_DELETE.ts +++ b/packages/client/lib/commands/FUNCTION_DELETE.ts @@ -1,7 +1,10 @@ -import { RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(library: string): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(library: RedisArgument) { return ['FUNCTION', 'DELETE', library]; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_DUMP.spec.ts b/packages/client/lib/commands/FUNCTION_DUMP.spec.ts index 360ec6b7453..4d4e885e4f2 100644 --- a/packages/client/lib/commands/FUNCTION_DUMP.spec.ts +++ b/packages/client/lib/commands/FUNCTION_DUMP.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './FUNCTION_DUMP'; +import FUNCTION_DUMP from './FUNCTION_DUMP'; describe('FUNCTION DUMP', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'DUMP'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + FUNCTION_DUMP.transformArguments(), + ['FUNCTION', 'DUMP'] + ); + }); - testUtils.testWithClient('client.functionDump', async client => { - assert.equal( - typeof await client.functionDump(), - 'string' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.functionDump', async client => { + assert.equal( + typeof await client.functionDump(), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FUNCTION_DUMP.ts b/packages/client/lib/commands/FUNCTION_DUMP.ts index f608e078c27..8f6ff047fa7 100644 --- a/packages/client/lib/commands/FUNCTION_DUMP.ts +++ b/packages/client/lib/commands/FUNCTION_DUMP.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['FUNCTION', 'DUMP']; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts b/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts index 12009d03363..5601784ed6a 100644 --- a/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts +++ b/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts @@ -1,30 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './FUNCTION_FLUSH'; +import FUNCTION_FLUSH from './FUNCTION_FLUSH'; describe('FUNCTION FLUSH', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'FLUSH'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + FUNCTION_FLUSH.transformArguments(), + ['FUNCTION', 'FLUSH'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments('SYNC'), - ['FUNCTION', 'FLUSH', 'SYNC'] - ); - }); + it('with mode', () => { + assert.deepEqual( + FUNCTION_FLUSH.transformArguments('SYNC'), + ['FUNCTION', 'FLUSH', 'SYNC'] + ); }); + }); - testUtils.testWithClient('client.functionFlush', async client => { - assert.equal( - await client.functionFlush(), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.functionFlush', async client => { + assert.equal( + await client.functionFlush(), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.ts b/packages/client/lib/commands/FUNCTION_FLUSH.ts index 143282de97f..844d3586d90 100644 --- a/packages/client/lib/commands/FUNCTION_FLUSH.ts +++ b/packages/client/lib/commands/FUNCTION_FLUSH.ts @@ -1,13 +1,17 @@ -import { RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { RedisFlushMode } from './FLUSHALL'; -export function transformArguments(mode?: 'ASYNC' | 'SYNC'): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(mode?: RedisFlushMode) { const args = ['FUNCTION', 'FLUSH']; - + if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_KILL.spec.ts b/packages/client/lib/commands/FUNCTION_KILL.spec.ts index df4848fc82e..be231e41180 100644 --- a/packages/client/lib/commands/FUNCTION_KILL.spec.ts +++ b/packages/client/lib/commands/FUNCTION_KILL.spec.ts @@ -1,14 +1,14 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; -import { transformArguments } from './FUNCTION_KILL'; +import FUNCTION_KILL from './FUNCTION_KILL'; describe('FUNCTION KILL', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'KILL'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + FUNCTION_KILL.transformArguments(), + ['FUNCTION', 'KILL'] + ); + }); }); diff --git a/packages/client/lib/commands/FUNCTION_KILL.ts b/packages/client/lib/commands/FUNCTION_KILL.ts index 517272e8376..f452b0b80d9 100644 --- a/packages/client/lib/commands/FUNCTION_KILL.ts +++ b/packages/client/lib/commands/FUNCTION_KILL.ts @@ -1,7 +1,10 @@ -import { RedisCommandArguments } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['FUNCTION', 'KILL']; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_LIST.spec.ts b/packages/client/lib/commands/FUNCTION_LIST.spec.ts index 80723d070de..e269d3150b6 100644 --- a/packages/client/lib/commands/FUNCTION_LIST.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LIST.spec.ts @@ -1,41 +1,45 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; -import { transformArguments } from './FUNCTION_LIST'; +import FUNCTION_LIST from './FUNCTION_LIST'; +import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; describe('FUNCTION LIST', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'LIST'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + FUNCTION_LIST.transformArguments(), + ['FUNCTION', 'LIST'] + ); + }); - it('with pattern', () => { - assert.deepEqual( - transformArguments('patter*'), - ['FUNCTION', 'LIST', 'patter*'] - ); - }); + it('with LIBRARYNAME', () => { + assert.deepEqual( + FUNCTION_LIST.transformArguments({ + LIBRARYNAME: 'patter*' + }), + ['FUNCTION', 'LIST', 'LIBRARYNAME', 'patter*'] + ); }); + }); + + testUtils.testWithClient('client.functionList', async client => { + const [, reply] = await Promise.all([ + loadMathFunction(client), + client.functionList() + ]); - testUtils.testWithClient('client.functionList', async client => { - await loadMathFunction(client); + reply[0].library_name; - assert.deepEqual( - await client.functionList(), - [{ - libraryName: MATH_FUNCTION.name, - engine: MATH_FUNCTION.engine, - functions: [{ - name: MATH_FUNCTION.library.square.NAME, - description: null, - flags: ['no-writes'] - }] - }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [{ + library_name: MATH_FUNCTION.name, + engine: MATH_FUNCTION.engine, + functions: [{ + name: MATH_FUNCTION.library.square.NAME, + description: null, + flags: ['no-writes'] + }] + }]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FUNCTION_LIST.ts b/packages/client/lib/commands/FUNCTION_LIST.ts index d6a39dc726d..07c1ff2a000 100644 --- a/packages/client/lib/commands/FUNCTION_LIST.ts +++ b/packages/client/lib/commands/FUNCTION_LIST.ts @@ -1,16 +1,51 @@ -import { RedisCommandArguments } from '.'; -import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers'; +import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, NullReply, SetReply, UnwrapReply, Resp2Reply, CommandArguments, Command } from '../RESP/types'; -export function transformArguments(pattern?: string): RedisCommandArguments { - const args = ['FUNCTION', 'LIST']; +export interface FunctionListOptions { + LIBRARYNAME?: RedisArgument; +} + +export type FunctionListReplyItem = [ + [BlobStringReply<'library_name'>, BlobStringReply | NullReply], + [BlobStringReply<'engine'>, BlobStringReply], + [BlobStringReply<'functions'>, ArrayReply, BlobStringReply], + [BlobStringReply<'description'>, BlobStringReply | NullReply], + [BlobStringReply<'flags'>, SetReply], + ]>>] +]; + +export type FunctionListReply = ArrayReply>; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(options?: FunctionListOptions) { + const args: CommandArguments = ['FUNCTION', 'LIST']; - if (pattern) { - args.push(pattern); + if (options?.LIBRARYNAME) { + args.push('LIBRARYNAME', options.LIBRARYNAME); } return args; -} - -export function transformReply(reply: Array): Array { - return reply.map(transformFunctionListItemReply); -} + }, + transformReply: { + 2: (reply: UnwrapReply>) => { + return reply.map(library => { + const unwrapped = library as unknown as UnwrapReply; + return { + library_name: unwrapped[1], + engine: unwrapped[3], + functions: (unwrapped[5] as unknown as UnwrapReply).map(fn => { + const unwrapped = fn as unknown as UnwrapReply; + return { + name: unwrapped[1], + description: unwrapped[3], + flags: unwrapped[5] + }; + }) + }; + }); + }, + 3: undefined as unknown as () => FunctionListReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts index 56e6102a4b4..8ff40582460 100644 --- a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts @@ -1,42 +1,48 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { MATH_FUNCTION, loadMathFunction } from '../client/index.spec'; -import { transformArguments } from './FUNCTION_LIST_WITHCODE'; +import FUNCTION_LIST_WITHCODE from './FUNCTION_LIST_WITHCODE'; +import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; describe('FUNCTION LIST WITHCODE', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'LIST', 'WITHCODE'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + FUNCTION_LIST_WITHCODE.transformArguments(), + ['FUNCTION', 'LIST', 'WITHCODE'] + ); + }); - it('with pattern', () => { - assert.deepEqual( - transformArguments('patter*'), - ['FUNCTION', 'LIST', 'patter*', 'WITHCODE'] - ); - }); + it('with LIBRARYNAME', () => { + assert.deepEqual( + FUNCTION_LIST_WITHCODE.transformArguments({ + LIBRARYNAME: 'patter*' + }), + ['FUNCTION', 'LIST', 'LIBRARYNAME', 'patter*', 'WITHCODE'] + ); }); + }); + + testUtils.testWithClient('client.functionListWithCode', async client => { + const [, reply] = await Promise.all([ + loadMathFunction(client), + client.functionListWithCode() + ]); - testUtils.testWithClient('client.functionListWithCode', async client => { - await loadMathFunction(client); + const a = reply[0]; - assert.deepEqual( - await client.functionListWithCode(), - [{ - libraryName: MATH_FUNCTION.name, - engine: MATH_FUNCTION.engine, - functions: [{ - name: MATH_FUNCTION.library.square.NAME, - description: null, - flags: ['no-writes'] - }], - libraryCode: MATH_FUNCTION.code - }] - ); - }, GLOBAL.SERVERS.OPEN); + const b = a.functions[0].description; + + assert.deepEqual(reply, [{ + library_name: MATH_FUNCTION.name, + engine: MATH_FUNCTION.engine, + functions: [{ + name: MATH_FUNCTION.library.square.NAME, + description: null, + flags: ['no-writes'] + }], + library_code: MATH_FUNCTION.code + }]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts index 0d763301e87..47a02a3da8a 100644 --- a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts +++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts @@ -1,26 +1,38 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformFunctionListArguments } from './FUNCTION_LIST'; -import { FunctionListItemReply, FunctionListRawItemReply, transformFunctionListItemReply } from './generic-transformers'; +import { TuplesToMapReply, BlobStringReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; +import FUNCTION_LIST, { FunctionListReplyItem } from './FUNCTION_LIST'; -export function transformArguments(pattern?: string): RedisCommandArguments { - const args = transformFunctionListArguments(pattern); - args.push('WITHCODE'); - return args; -} +export type FunctionListWithCodeReply = ArrayReply, BlobStringReply], +]>>; -type FunctionListWithCodeRawItemReply = [ - ...FunctionListRawItemReply, - 'library_code', - string -]; - -interface FunctionListWithCodeItemReply extends FunctionListItemReply { - libraryCode: string; -} - -export function transformReply(reply: Array): Array { - return reply.map(library => ({ - ...transformFunctionListItemReply(library as unknown as FunctionListRawItemReply), - libraryCode: library[7] - })); -} +export default { + FIRST_KEY_INDEX: FUNCTION_LIST.FIRST_KEY_INDEX, + IS_READ_ONLY: FUNCTION_LIST.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = FUNCTION_LIST.transformArguments(...args); + redisArgs.push('WITHCODE'); + return redisArgs; + }, + transformReply: { + 2: (reply: UnwrapReply>) => { + return reply.map(library => { + const unwrapped = library as unknown as UnwrapReply; + return { + library_name: unwrapped[1], + engine: unwrapped[3], + functions: (unwrapped[5] as unknown as UnwrapReply).map(fn => { + const unwrapped = fn as unknown as UnwrapReply; + return { + name: unwrapped[1], + description: unwrapped[3], + flags: unwrapped[5] + }; + }), + library_code: unwrapped[7] + }; + }); + }, + 3: undefined as unknown as () => FunctionListWithCodeReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts index 7be371c6b9c..fe896bdf8c8 100644 --- a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts @@ -1,36 +1,77 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { MATH_FUNCTION } from '../client/index.spec'; -import { transformArguments } from './FUNCTION_LOAD'; +import FUNCTION_LOAD from './FUNCTION_LOAD'; +import { RedisClientType } from '../client'; +import { NumberReply, RedisFunctions, RedisModules, RedisScripts, RespVersions } from '../RESP/types'; + + + +export const MATH_FUNCTION = { + name: 'math', + engine: 'LUA', + code: + `#!LUA name=math + redis.register_function { + function_name = "square", + callback = function(keys, args) + local number = redis.call('GET', keys[1]) + return number * number + end, + flags = { "no-writes" } + }`, + library: { + square: { + NAME: 'square', + IS_READ_ONLY: true, + NUMBER_OF_KEYS: 1, + FIRST_KEY_INDEX: 0, + transformArguments(key: string) { + return [key]; + }, + transformReply: undefined as unknown as () => NumberReply + } + } +}; + +export function loadMathFunction< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions +>( + client: RedisClientType +) { + return client.functionLoad( + MATH_FUNCTION.code, + { REPLACE: true } + ); +} describe('FUNCTION LOAD', () => { - testUtils.isVersionGreaterThanHook([7]); - - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments( 'code'), - ['FUNCTION', 'LOAD', 'code'] - ); - }); - - it('with REPLACE', () => { - assert.deepEqual( - transformArguments('code', { - REPLACE: true - }), - ['FUNCTION', 'LOAD', 'REPLACE', 'code'] - ); - }); + testUtils.isVersionGreaterThanHook([7]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + FUNCTION_LOAD.transformArguments('code'), + ['FUNCTION', 'LOAD', 'code'] + ); + }); + + it('with REPLACE', () => { + assert.deepEqual( + FUNCTION_LOAD.transformArguments('code', { + REPLACE: true + }), + ['FUNCTION', 'LOAD', 'REPLACE', 'code'] + ); }); + }); - testUtils.testWithClient('client.functionLoad', async client => { - assert.equal( - await client.functionLoad( - MATH_FUNCTION.code, - { REPLACE: true } - ), - MATH_FUNCTION.name - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.functionLoad', async client => { + assert.equal( + await loadMathFunction(client), + MATH_FUNCTION.name + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FUNCTION_LOAD.ts b/packages/client/lib/commands/FUNCTION_LOAD.ts index 7ab58d58598..32b030909ad 100644 --- a/packages/client/lib/commands/FUNCTION_LOAD.ts +++ b/packages/client/lib/commands/FUNCTION_LOAD.ts @@ -1,22 +1,22 @@ -import { RedisCommandArguments } from '.'; +import { RedisArgument, CommandArguments, BlobStringReply, Command } from '../RESP/types'; -interface FunctionLoadOptions { - REPLACE?: boolean; +export interface FunctionLoadOptions { + REPLACE?: boolean; } -export function transformArguments( - code: string, - options?: FunctionLoadOptions -): RedisCommandArguments { - const args = ['FUNCTION', 'LOAD']; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(code: RedisArgument, options?: FunctionLoadOptions) { + const args: CommandArguments = ['FUNCTION', 'LOAD']; if (options?.REPLACE) { - args.push('REPLACE'); + args.push('REPLACE'); } args.push(code); return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts b/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts index a5c2e2dcc72..465e99b6104 100644 --- a/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts +++ b/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts @@ -1,37 +1,40 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './FUNCTION_RESTORE'; +import FUNCTION_RESTORE from './FUNCTION_RESTORE'; +import { RESP_TYPES } from '../RESP/decoder'; describe('FUNCTION RESTORE', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('dump'), - ['FUNCTION', 'RESTORE', 'dump'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + FUNCTION_RESTORE.transformArguments('dump'), + ['FUNCTION', 'RESTORE', 'dump'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments('dump', 'APPEND'), - ['FUNCTION', 'RESTORE', 'dump', 'APPEND'] - ); - }); + it('with mode', () => { + assert.deepEqual( + FUNCTION_RESTORE.transformArguments('dump', { + mode: 'APPEND' + }), + ['FUNCTION', 'RESTORE', 'dump', 'APPEND'] + ); }); + }); - testUtils.testWithClient('client.functionRestore', async client => { - assert.equal( - await client.functionRestore( - await client.functionDump( - client.commandOptions({ - returnBuffers: true - }) - ), - 'FLUSH' - ), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.functionRestore', async client => { + assert.equal( + await client.functionRestore( + await client.withTypeMapping({ + [RESP_TYPES.BLOB_STRING]: Buffer + }).functionDump(), + { + mode: 'REPLACE' + } + ), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.ts b/packages/client/lib/commands/FUNCTION_RESTORE.ts index bc9c41e262d..8c875530562 100644 --- a/packages/client/lib/commands/FUNCTION_RESTORE.ts +++ b/packages/client/lib/commands/FUNCTION_RESTORE.ts @@ -1,16 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { SimpleStringReply, Command, RedisArgument } from '../RESP/types'; -export function transformArguments( - dump: RedisCommandArgument, - mode?: 'FLUSH' | 'APPEND' | 'REPLACE' -): RedisCommandArguments { +export interface FunctionRestoreOptions { + mode?: 'FLUSH' | 'APPEND' | 'REPLACE'; +} + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(dump: RedisArgument, options?: FunctionRestoreOptions) { const args = ['FUNCTION', 'RESTORE', dump]; - if (mode) { - args.push(mode); + if (options?.mode) { + args.push(options.mode); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_STATS.spec.ts b/packages/client/lib/commands/FUNCTION_STATS.spec.ts index a5e26b5fecc..b48b012614f 100644 --- a/packages/client/lib/commands/FUNCTION_STATS.spec.ts +++ b/packages/client/lib/commands/FUNCTION_STATS.spec.ts @@ -1,25 +1,25 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './FUNCTION_STATS'; +import FUNCTION_STATS from './FUNCTION_STATS'; describe('FUNCTION STATS', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['FUNCTION', 'STATS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + FUNCTION_STATS.transformArguments(), + ['FUNCTION', 'STATS'] + ); + }); - testUtils.testWithClient('client.functionStats', async client => { - const stats = await client.functionStats(); - assert.equal(stats.runningScript, null); - assert.equal(typeof stats.engines, 'object'); - for (const [engine, { librariesCount, functionsCount }] of Object.entries(stats.engines)) { - assert.equal(typeof engine, 'string'); - assert.equal(typeof librariesCount, 'number'); - assert.equal(typeof functionsCount, 'number'); - } - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.functionStats', async client => { + const stats = await client.functionStats(); + assert.equal(stats.running_script, null); + assert.equal(typeof stats.engines, 'object'); + for (const [engine, { libraries_count, functions_count }] of Object.entries(stats.engines)) { + assert.equal(typeof engine, 'string'); + assert.equal(typeof libraries_count, 'number'); + assert.equal(typeof functions_count, 'number'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/FUNCTION_STATS.ts b/packages/client/lib/commands/FUNCTION_STATS.ts index bb5c957e78b..138d1fb82d5 100644 --- a/packages/client/lib/commands/FUNCTION_STATS.ts +++ b/packages/client/lib/commands/FUNCTION_STATS.ts @@ -1,56 +1,70 @@ -import { RedisCommandArguments } from '.'; +import { Command, TuplesToMapReply, BlobStringReply, NullReply, NumberReply, MapReply, Resp2Reply, UnwrapReply } from '../RESP/types'; +import { isNullReply } from './generic-transformers'; -export function transformArguments(): RedisCommandArguments { +type RunningScript = NullReply | TuplesToMapReply<[ + [BlobStringReply<'name'>, BlobStringReply], + [BlobStringReply<'command'>, BlobStringReply], + [BlobStringReply<'duration_ms'>, NumberReply] +]>; + +type Engine = TuplesToMapReply<[ + [BlobStringReply<'libraries_count'>, NumberReply], + [BlobStringReply<'functions_count'>, NumberReply] +]>; + +type Engines = MapReply; + +type FunctionStatsReply = TuplesToMapReply<[ + [BlobStringReply<'running_script'>, RunningScript], + [BlobStringReply<'engines'>, Engines] +]>; + +export default { + IS_READ_ONLY: true, + FIRST_KEY_INDEX: undefined, + transformArguments() { return ['FUNCTION', 'STATS']; -} + }, + transformReply: { + 2: (reply: UnwrapReply>) => { + return { + running_script: transformRunningScript(reply[1]), + engines: transformEngines(reply[3]) + }; + }, + 3: undefined as unknown as () => FunctionStatsReply + } +} as const satisfies Command; -type FunctionStatsRawReply = [ - 'running_script', - null | [ - 'name', - string, - 'command', - string, - 'duration_ms', - number - ], - 'engines', - Array // "flat tuples" (there is no way to type that) - // ...[string, [ - // 'libraries_count', - // number, - // 'functions_count', - // number - // ]] -]; - -interface FunctionStatsReply { - runningScript: null | { - name: string; - command: string; - durationMs: number; - }; - engines: Record; +function transformRunningScript(reply: Resp2Reply) { + if (isNullReply(reply)) { + return null; + } + + const unwraped = reply as unknown as UnwrapReply; + return { + name: unwraped[1], + command: unwraped[3], + duration_ms: unwraped[5] + }; } -export function transformReply(reply: FunctionStatsRawReply): FunctionStatsReply { - const engines = Object.create(null); - for (let i = 0; i < reply[3].length; i++) { - engines[reply[3][i]] = { - librariesCount: reply[3][++i][1], - functionsCount: reply[3][i][3] - }; - } - - return { - runningScript: reply[1] === null ? null : { - name: reply[1][1], - command: reply[1][3], - durationMs: reply[1][5] - }, - engines +function transformEngines(reply: Resp2Reply) { + const unwraped = reply as unknown as UnwrapReply; + + const engines: Record = Object.create(null); + for (let i = 0; i < unwraped.length; i++) { + const name = unwraped[i] as BlobStringReply, + stats = unwraped[++i] as Resp2Reply, + unwrapedStats = stats as unknown as UnwrapReply; + engines[name.toString()] = { + libraries_count: unwrapedStats[1], + functions_count: unwrapedStats[3] }; + } + + return engines; } diff --git a/packages/client/lib/commands/GEOADD.spec.ts b/packages/client/lib/commands/GEOADD.spec.ts index 6425c881c9d..14195ed289c 100644 --- a/packages/client/lib/commands/GEOADD.spec.ts +++ b/packages/client/lib/commands/GEOADD.spec.ts @@ -1,95 +1,100 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GEOADD'; +import GEOADD from './GEOADD'; describe('GEOADD', () => { - describe('transformArguments', () => { - it('one member', () => { - assert.deepEqual( - transformArguments('key', { - member: 'member', - longitude: 1, - latitude: 2 - }), - ['GEOADD', 'key', '1', '2', 'member'] - ); - }); + describe('transformArguments', () => { + it('one member', () => { + assert.deepEqual( + GEOADD.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('multiple members', () => { + assert.deepEqual( + GEOADD.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 condition', () => { + assert.deepEqual( + GEOADD.transformArguments('key', { + longitude: 1, + latitude: 2, + member: 'member' + }, { + condition: 'NX' + }), + ['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 NX (backwards compatibility)', () => { + assert.deepEqual( + GEOADD.transformArguments('key', { + longitude: 1, + latitude: 2, + member: 'member' + }, { + NX: true + }), + ['GEOADD', 'key', 'NX', '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'] - ); - }); + it('with CH', () => { + assert.deepEqual( + GEOADD.transformArguments('key', { + longitude: 1, + latitude: 2, + member: 'member' + }, { + CH: true + }), + ['GEOADD', 'key', 'CH', '1', '2', 'member'] + ); }); - testUtils.testWithClient('client.geoAdd', async client => { - assert.equal( - await client.geoAdd('key', { - member: 'member', - longitude: 1, - latitude: 2 - }), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + it('with condition, CH', () => { + assert.deepEqual( + GEOADD.transformArguments('key', { + longitude: 1, + latitude: 2, + member: 'member' + }, { + condition: 'XX', + CH: true + }), + ['GEOADD', 'key', 'XX', 'CH', '1', '2', 'member'] + ); + }); + }); - testUtils.testWithCluster('cluster.geoAdd', async cluster => { - assert.equal( - await cluster.geoAdd('key', { - member: 'member', - longitude: 1, - latitude: 2 - }), - 1 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('geoAdd', async client => { + assert.equal( + await client.geoAdd('key', { + member: 'member', + longitude: 1, + latitude: 2 + }), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEOADD.ts b/packages/client/lib/commands/GEOADD.ts index daccb0842e0..f89f6b80e82 100644 --- a/packages/client/lib/commands/GEOADD.ts +++ b/packages/client/lib/commands/GEOADD.ts @@ -1,53 +1,65 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoCoordinates } from './generic-transformers'; +import { RedisArgument, CommandArguments, NumberReply, Command } from '../RESP/types'; +import { GeoCoordinates } from './GEOSEARCH'; -export const FIRST_KEY_INDEX = 1; - -interface GeoMember extends GeoCoordinates { - member: RedisCommandArgument; -} - -interface NX { - NX?: true; +export interface GeoMember extends GeoCoordinates { + member: RedisArgument; } -interface XX { - XX?: true; +export interface GeoAddOptions { + condition?: 'NX' | 'XX'; + /** + * @deprecated Use `{ condition: 'NX' }` instead. + */ + NX?: boolean; + /** + * @deprecated Use `{ condition: 'XX' }` instead. + */ + XX?: boolean; + CH?: boolean; } -type SetGuards = NX | XX; - -interface GeoAddCommonOptions { - CH?: true; -} - -type GeoAddOptions = SetGuards & GeoAddCommonOptions; - -export function transformArguments( - key: RedisCommandArgument, toAdd: GeoMember | Array, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + toAdd: GeoMember | Array, options?: GeoAddOptions -): RedisCommandArguments { + ) { const args = ['GEOADD', key]; - if ((options as NX)?.NX) { - args.push('NX'); - } else if ((options as XX)?.XX) { - args.push('XX'); + if (options?.condition) { + args.push(options.condition); + } else if (options?.NX) { + args.push('NX'); + } else if (options?.XX) { + args.push('XX'); } if (options?.CH) { - args.push('CH'); + args.push('CH'); } - for (const { longitude, latitude, member } of (Array.isArray(toAdd) ? toAdd : [toAdd])) { - args.push( - longitude.toString(), - latitude.toString(), - member - ); + if (Array.isArray(toAdd)) { + for (const member of toAdd) { + pushMember(args, member); + } + } else { + pushMember(args, toAdd); } return args; -} + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; -export declare function transformReply(): number; +function pushMember( + args: CommandArguments, + { longitude, latitude, member }: GeoMember +) { + args.push( + longitude.toString(), + latitude.toString(), + member + ); +} diff --git a/packages/client/lib/commands/GEODIST.spec.ts b/packages/client/lib/commands/GEODIST.spec.ts index bbc62480ee1..eb5a1ef801e 100644 --- a/packages/client/lib/commands/GEODIST.spec.ts +++ b/packages/client/lib/commands/GEODIST.spec.ts @@ -1,57 +1,54 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GEODIST'; +import GEODIST 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'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + GEODIST.transformArguments('key', '1', '2'), + ['GEODIST', 'key', '1', '2'] + ); }); - describe('client.geoDist', () => { - testUtils.testWithClient('null', async client => { - assert.equal( - await client.geoDist('key', '1', '2'), - null - ); - }, GLOBAL.SERVERS.OPEN); + it('with unit', () => { + assert.deepEqual( + GEODIST.transformArguments('key', '1', '2', 'm'), + ['GEODIST', 'key', '1', '2', 'm'] + ); + }); + }); - testUtils.testWithClient('with value', async client => { - const [, dist] = await Promise.all([ - client.geoAdd('key', [{ - member: '1', - longitude: 1, - latitude: 1 - }, { - member: '2', - longitude: 2, - latitude: 2 - }]), - client.geoDist('key', '1', '2') - ]); + testUtils.testAll('geoDist null', async client => { + assert.equal( + await client.geoDist('key', '1', '2'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); - assert.equal( - dist, - 157270.0561 - ); - }, GLOBAL.SERVERS.OPEN); - }); + testUtils.testAll('geoDist with member', async client => { + const [, dist] = await Promise.all([ + client.geoAdd('key', [{ + member: '1', + longitude: 1, + latitude: 1 + }, { + member: '2', + longitude: 2, + latitude: 2 + }]), + client.geoDist('key', '1', '2') + ]); - testUtils.testWithCluster('cluster.geoDist', async cluster => { - assert.equal( - await cluster.geoDist('key', '1', '2'), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + assert.equal( + dist, + 157270.0561 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEODIST.ts b/packages/client/lib/commands/GEODIST.ts index 5dbf8ece9cc..3e684d67579 100644 --- a/packages/client/lib/commands/GEODIST.ts +++ b/packages/client/lib/commands/GEODIST.ts @@ -1,25 +1,24 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoUnits } from './generic-transformers'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; +import { GeoUnits } from './GEOSEARCH'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member1: RedisCommandArgument, - member2: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + member1: RedisArgument, + member2: RedisArgument, unit?: GeoUnits -): RedisCommandArguments { + ) { const args = ['GEODIST', key, member1, member2]; if (unit) { - args.push(unit); + args.push(unit); } return args; -} - -export function transformReply(reply: RedisCommandArgument | null): number | null { + }, + transformReply(reply: BlobStringReply | NullReply) { return reply === null ? null : Number(reply); -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEOHASH.spec.ts b/packages/client/lib/commands/GEOHASH.spec.ts index c421c148f43..8efe55d89b6 100644 --- a/packages/client/lib/commands/GEOHASH.spec.ts +++ b/packages/client/lib/commands/GEOHASH.spec.ts @@ -1,35 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GEOHASH'; +import GEOHASH 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'] - ); - }); + describe('transformArguments', () => { + it('single member', () => { + assert.deepEqual( + GEOHASH.transformArguments('key', 'member'), + ['GEOHASH', 'key', 'member'] + ); }); - testUtils.testWithClient('client.geoHash', async client => { - assert.deepEqual( - await client.geoHash('key', 'member'), - [null] - ); - }, GLOBAL.SERVERS.OPEN); + it('multiple members', () => { + assert.deepEqual( + GEOHASH.transformArguments('key', ['1', '2']), + ['GEOHASH', 'key', '1', '2'] + ); + }); + }); - testUtils.testWithCluster('cluster.geoHash', async cluster => { - assert.deepEqual( - await cluster.geoHash('key', 'member'), - [null] - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('geoHash', async client => { + assert.deepEqual( + await client.geoHash('key', 'member'), + [null] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEOHASH.ts b/packages/client/lib/commands/GEOHASH.ts index 55e22c497e2..d8d2732e512 100644 --- a/packages/client/lib/commands/GEOHASH.ts +++ b/packages/client/lib/commands/GEOHASH.ts @@ -1,15 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['GEOHASH', key], member); -} - -export declare function transformReply(): Array; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + member: RedisVariadicArgument + ) { + return pushVariadicArguments(['GEOHASH', key], member); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEOPOS.spec.ts b/packages/client/lib/commands/GEOPOS.spec.ts index 9c08ccd08f5..20ad5c5c942 100644 --- a/packages/client/lib/commands/GEOPOS.spec.ts +++ b/packages/client/lib/commands/GEOPOS.spec.ts @@ -1,73 +1,51 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './GEOPOS'; +import GEOPOS 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'] - ); - }); + describe('transformArguments', () => { + it('single member', () => { + assert.deepEqual( + GEOPOS.transformArguments('key', 'member'), + ['GEOPOS', 'key', 'member'] + ); }); - describe('transformReply', () => { - it('null', () => { - assert.deepEqual( - transformReply([null]), - [null] - ); - }); - - it('with member', () => { - assert.deepEqual( - transformReply([['1', '2']]), - [{ - longitude: '1', - latitude: '2' - }] - ); - }); + it('multiple members', () => { + assert.deepEqual( + GEOPOS.transformArguments('key', ['1', '2']), + ['GEOPOS', 'key', '1', '2'] + ); }); - - describe('client.geoPos', () => { - testUtils.testWithClient('null', async client => { - assert.deepEqual( - await client.geoPos('key', 'member'), - [null] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('with member', async client => { - const coordinates = { - longitude: '-122.06429868936538696', - latitude: '37.37749628831998194' - }; - - await client.geoAdd('key', { - member: 'member', - ...coordinates - }); - - assert.deepEqual( - await client.geoPos('key', 'member'), - [coordinates] - ); - }, GLOBAL.SERVERS.OPEN); + }); + + testUtils.testAll('geoPos null', async client => { + assert.deepEqual( + await client.geoPos('key', 'member'), + [null] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testAll('geoPos with member', async client => { + const coordinates = { + longitude: '-122.06429868936538696', + latitude: '37.37749628831998194' + }; + + await client.geoAdd('key', { + member: 'member', + ...coordinates }); - testUtils.testWithCluster('cluster.geoPos', async cluster => { - assert.deepEqual( - await cluster.geoPos('key', 'member'), - [null] - ); - }, GLOBAL.CLUSTERS.OPEN); + assert.deepEqual( + await client.geoPos('key', 'member'), + [coordinates] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEOPOS.ts b/packages/client/lib/commands/GEOPOS.ts index 0a5f079deeb..30273c64c18 100644 --- a/packages/client/lib/commands/GEOPOS.ts +++ b/packages/client/lib/commands/GEOPOS.ts @@ -1,27 +1,22 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['GEOPOS', key], member); -} - -type GeoCoordinatesRawReply = Array<[RedisCommandArgument, RedisCommandArgument] | null>; - -interface GeoCoordinates { - longitude: RedisCommandArgument; - latitude: RedisCommandArgument; -} - -export function transformReply(reply: GeoCoordinatesRawReply): Array { - return reply.map(coordinates => coordinates === null ? null : { - longitude: coordinates[0], - latitude: coordinates[1] +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + member: RedisVariadicArgument + ) { + return pushVariadicArguments(['GEOPOS', key], member); + }, + transformReply(reply: UnwrapReply | NullReply>>) { + return reply.map(item => { + const unwrapped = item as unknown as UnwrapReply; + return unwrapped === null ? null : { + longitude: unwrapped[0], + latitude: unwrapped[1] + }; }); -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUS.spec.ts b/packages/client/lib/commands/GEORADIUS.spec.ts index 786b2665029..533e48f6898 100644 --- a/packages/client/lib/commands/GEORADIUS.spec.ts +++ b/packages/client/lib/commands/GEORADIUS.spec.ts @@ -1,35 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GEORADIUS'; +import GEORADIUS from './GEORADIUS'; describe('GEORADIUS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm'), - ['GEORADIUS', 'key', '1', '2', '3', 'm'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + GEORADIUS.transformArguments('key', { + longitude: 1, + latitude: 2 + }, 3, 'm'), + ['GEORADIUS', 'key', '1', '2', '3', 'm'] + ); + }); - testUtils.testWithClient('client.geoRadius', async client => { - assert.deepEqual( - await client.geoRadius('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm'), - [] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.geoRadius', async cluster => { - assert.deepEqual( - await cluster.geoRadius('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm'), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('geoRadius', async client => { + assert.deepEqual( + await client.geoRadius('key', { + longitude: 1, + latitude: 2 + }, 3, 'm'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEORADIUS.ts b/packages/client/lib/commands/GEORADIUS.ts index f47cf508848..e777432f090 100644 --- a/packages/client/lib/commands/GEORADIUS.ts +++ b/packages/client/lib/commands/GEORADIUS.ts @@ -1,25 +1,32 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchOptions, GeoCoordinates, pushGeoRadiusArguments, GeoUnits } from './generic-transformers'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { GeoCoordinates, GeoUnits, GeoSearchOptions, pushGeoSearchOptions } from './GEOSEARCH'; -export const FIRST_KEY_INDEX = 1; +export function transformGeoRadiusArguments( + command: RedisArgument, + key: RedisArgument, + from: GeoCoordinates, + radius: number, + unit: GeoUnits, + options?: GeoSearchOptions +) { + const args = [ + command, + key, + from.longitude.toString(), + from.latitude.toString(), + radius.toString(), + unit + ]; -export const IS_READ_ONLY = true; + pushGeoSearchOptions(args, options); -export function transformArguments( - key: RedisCommandArgument, - coordinates: GeoCoordinates, - radius: number, - unit: GeoUnits, - options?: GeoSearchOptions -): RedisCommandArguments { - return pushGeoRadiusArguments( - ['GEORADIUS'], - key, - coordinates, - radius, - unit, - options - ); + return args; } -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments: transformGeoRadiusArguments.bind(undefined, 'GEORADIUS'), + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; + diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts index 8cc4212c839..57349a79acb 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GEORADIUSBYMEMBER'; +import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER'; describe('GEORADIUSBYMEMBER', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member', 3 , 'm'), - ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + GEORADIUSBYMEMBER.transformArguments('key', 'member', 3, 'm'), + ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm'] + ); + }); - testUtils.testWithClient('client.geoRadiusByMember', async client => { - assert.deepEqual( - await client.geoRadiusByMember('key', 'member', 3 , 'm'), - [] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.geoRadiusByMember', async cluster => { - assert.deepEqual( - await cluster.geoRadiusByMember('key', 'member', 3 , 'm'), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('geoRadiusByMember', async client => { + assert.deepEqual( + await client.geoRadiusByMember('key', 'member', 3, 'm'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts index 96bb622fb85..13b52a30630 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts @@ -1,25 +1,30 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchOptions, pushGeoRadiusArguments, GeoUnits } from './generic-transformers'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { GeoUnits, GeoSearchOptions, pushGeoSearchOptions } from './GEOSEARCH'; -export const FIRST_KEY_INDEX = 1; +export function transformGeoRadiusByMemberArguments( + command: RedisArgument, + key: RedisArgument, + from: RedisArgument, + radius: number, + unit: GeoUnits, + options?: GeoSearchOptions +) { + const args = [ + command, + key, + from, + radius.toString(), + unit + ]; -export const IS_READ_ONLY = true; + pushGeoSearchOptions(args, options); -export function transformArguments( - key: RedisCommandArgument, - member: string, - radius: number, - unit: GeoUnits, - options?: GeoSearchOptions -): RedisCommandArguments { - return pushGeoRadiusArguments( - ['GEORADIUSBYMEMBER'], - key, - member, - radius, - unit, - options - ); + return args; } -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments: transformGeoRadiusByMemberArguments.bind(undefined, 'GEORADIUSBYMEMBER'), + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.spec.ts deleted file mode 100644 index 100ecc03368..00000000000 --- a/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './GEORADIUSBYMEMBERSTORE'; - -describe('GEORADIUSBYMEMBERSTORE', () => { - describe('transformArguments', () => { - it('STORE', () => { - assert.deepEqual( - transformArguments('key', 'member', 3 , 'm', 'dest', { - SORT: 'ASC', - COUNT: { - value: 1, - ANY: true - } - }), - ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'ASC', 'COUNT', '1', 'ANY', 'STORE', 'dest'] - ); - }); - - it('STOREDIST', () => { - assert.deepEqual( - transformArguments('key', 'member', 3 , 'm', 'dest', { STOREDIST: true }), - ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'STOREDIST', 'dest'] - ); - }); - }); - - testUtils.testWithClient('client.geoRadiusByMemberStore', async client => { - await client.geoAdd('source', { - longitude: 1, - latitude: 1, - member: 'member' - }); - - assert.equal( - await client.geoRadiusByMemberStore('source', 'member', 3 , 'm', 'dest'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.geoRadiusByMemberStore', async cluster => { - await cluster.geoAdd('{tag}source', { - longitude: 1, - latitude: 1, - member: 'member' - }); - - assert.equal( - await cluster.geoRadiusByMemberStore('{tag}source', 'member', 3 , 'm','{tag}destination'), - 1 - ); - }, GLOBAL.CLUSTERS.OPEN); -}); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.ts b/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.ts deleted file mode 100644 index 28f3c25fac9..00000000000 --- a/packages/client/lib/commands/GEORADIUSBYMEMBERSTORE.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoUnits, GeoRadiusStoreOptions, pushGeoRadiusStoreArguments } from './generic-transformers'; - -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUSBYMEMBER'; - -export function transformArguments( - key: RedisCommandArgument, - member: string, - radius: number, - unit: GeoUnits, - destination: RedisCommandArgument, - options?: GeoRadiusStoreOptions, -): RedisCommandArguments { - return pushGeoRadiusStoreArguments( - ['GEORADIUSBYMEMBER'], - key, - member, - radius, - unit, - destination, - options - ); -} - -export declare function transformReply(): number diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts index f3a47856e86..abf10013973 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GEORADIUSBYMEMBER_RO'; +import GEORADIUSBYMEMBER_RO from './GEORADIUSBYMEMBER_RO'; describe('GEORADIUSBYMEMBER_RO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member', 3 , 'm'), - ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + GEORADIUSBYMEMBER_RO.transformArguments('key', 'member', 3, 'm'), + ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm'] + ); + }); - testUtils.testWithClient('client.geoRadiusByMemberRo', async client => { - assert.deepEqual( - await client.geoRadiusByMemberRo('key', 'member', 3 , 'm'), - [] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.geoRadiusByMemberRo', async cluster => { - assert.deepEqual( - await cluster.geoRadiusByMemberRo('key', 'member', 3 , 'm'), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('geoRadiusByMemberRo', async client => { + assert.deepEqual( + await client.geoRadiusByMemberRo('key', 'member', 3, 'm'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts index 63f29ae65b5..7f85ed15df7 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts @@ -1,25 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchOptions, pushGeoRadiusArguments, GeoUnits } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: string, - radius: number, - unit: GeoUnits, - options?: GeoSearchOptions -): RedisCommandArguments { - return pushGeoRadiusArguments( - ['GEORADIUSBYMEMBER_RO'], - key, - member, - radius, - unit, - options - ); -} - -export declare function transformReply(): Array; +import { Command } from '../RESP/types'; +import GEORADIUSBYMEMBER, { transformGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; + +export default { + FIRST_KEY_INDEX: GEORADIUSBYMEMBER.FIRST_KEY_INDEX, + IS_READ_ONLY: true, + transformArguments: transformGeoRadiusByMemberArguments.bind(undefined, 'GEORADIUSBYMEMBER_RO'), + transformReply: GEORADIUSBYMEMBER.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts index 7904a763998..bcf91266365 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts @@ -1,31 +1,44 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { RedisCommandArguments } from '.'; -import { GeoReplyWith } from './generic-transformers'; -import { transformArguments } from './GEORADIUSBYMEMBER_RO_WITH'; +import GEORADIUSBYMEMBER_RO_WITH from './GEORADIUSBYMEMBER_RO_WITH'; +import { CommandArguments } from '../RESP/types'; +import { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; describe('GEORADIUSBYMEMBER_RO WITH', () => { - it('transformArguments', () => { - const expectedReply: RedisCommandArguments = ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm', 'WITHDIST']; - expectedReply.preserve = ['WITHDIST']; + it('transformArguments', () => { + const expectedReply: CommandArguments = ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm', 'WITHDIST']; + expectedReply.preserve = ['WITHDIST']; - assert.deepEqual( - transformArguments('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]), - expectedReply - ); - }); + assert.deepEqual( + GEORADIUSBYMEMBER_RO_WITH.transformArguments('key', 'member', 3, 'm', [ + GEO_REPLY_WITH.DISTANCE + ]), + expectedReply + ); + }); - testUtils.testWithClient('client.geoRadiusByMemberRoWith', async client => { - assert.deepEqual( - await client.geoRadiusByMemberRoWith('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('geoRadiusByMemberRoWith', async client => { + const [, reply] = await Promise.all([ + client.geoAdd('key', { + member: 'member', + longitude: 1, + latitude: 2 + }), + client.geoRadiusByMemberRoWith('key', 'member', 1, 'm', [ + GEO_REPLY_WITH.HASH, + GEO_REPLY_WITH.DISTANCE, + GEO_REPLY_WITH.COORDINATES + ]) + ]); - testUtils.testWithCluster('cluster.geoRadiusByMemberRoWith', async cluster => { - assert.deepEqual( - await cluster.geoRadiusByMemberRoWith('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + assert.equal(reply.length, 1); + assert.equal(reply[0].member, 'member'); + assert.equal(typeof reply[0].distance, 'string'); + assert.equal(typeof reply[0].hash, 'number'); + assert.equal(typeof reply[0].coordinates?.longitude, 'string'); + assert.equal(typeof reply[0].coordinates?.latitude, 'string'); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts index 6061be734b5..5fb945a1f9c 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts @@ -1,30 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoReplyWith, GeoSearchOptions, GeoUnits } from './generic-transformers'; -import { transformArguments as geoRadiusTransformArguments } from './GEORADIUSBYMEMBER_RO'; - -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUSBYMEMBER_RO'; - -export function transformArguments( - key: RedisCommandArgument, - member: string, - radius: number, - unit: GeoUnits, - replyWith: Array, - options?: GeoSearchOptions -): RedisCommandArguments { - const args: RedisCommandArguments = geoRadiusTransformArguments( - key, - member, - radius, - unit, - options - ); - - args.push(...replyWith); - - args.preserve = replyWith; - - return args; -} - -export { transformGeoMembersWithReply as transformReply } from './generic-transformers'; +import { Command } from '../RESP/types'; +import GEORADIUSBYMEMBER_WITH, { transformGeoRadiusByMemberWithArguments } from './GEORADIUSBYMEMBER_WITH'; + +export default { + FIRST_KEY_INDEX: GEORADIUSBYMEMBER_WITH.FIRST_KEY_INDEX, + IS_READ_ONLY: true, + transformArguments: transformGeoRadiusByMemberWithArguments.bind(undefined, 'GEORADIUSBYMEMBER_RO'), + transformReply: GEORADIUSBYMEMBER_WITH.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts new file mode 100644 index 00000000000..3d44060f205 --- /dev/null +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts @@ -0,0 +1,39 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import GEORADIUSBYMEMBER_STORE from './GEORADIUSBYMEMBER_STORE'; + +describe('GEORADIUSBYMEMBER STORE', () => { + describe('transformArguments', () => { + it('STORE', () => { + assert.deepEqual( + GEORADIUSBYMEMBER_STORE.transformArguments('key', 'member', 3, 'm', 'destination'), + ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'STORE', 'destination'] + ); + }); + + it('STOREDIST', () => { + assert.deepEqual( + GEORADIUSBYMEMBER_STORE.transformArguments('key', 'member', 3, 'm', 'destination', { + STOREDIST: true + }), + ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'STOREDIST', 'destination'] + ); + }); + }); + + testUtils.testAll('geoRadiusByMemberStore', async client => { + const [, reply] = await Promise.all([ + client.geoAdd('{tag}source', { + longitude: 1, + latitude: 2, + member: 'member' + }), + client.geoRadiusByMemberStore('{tag}source', 'member', 3, 'm', '{tag}destination') + ]); + + assert.equal(reply, 1); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts new file mode 100644 index 00000000000..90419963110 --- /dev/null +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts @@ -0,0 +1,31 @@ +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import GEORADIUSBYMEMBER, { transformGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; +import { GeoSearchOptions, GeoUnits } from './GEOSEARCH'; + +export interface GeoRadiusStoreOptions extends GeoSearchOptions { + STOREDIST?: boolean; +} + +export default { + FIRST_KEY_INDEX: GEORADIUSBYMEMBER.FIRST_KEY_INDEX, + IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, + transformArguments( + key: RedisArgument, + from: RedisArgument, + radius: number, + unit: GeoUnits, + destination: RedisArgument, + options?: GeoRadiusStoreOptions + ) { + const args = transformGeoRadiusByMemberArguments('GEORADIUSBYMEMBER', key, from, radius, unit, options); + + if (options?.STOREDIST) { + args.push('STOREDIST', destination); + } else { + args.push('STORE', destination); + } + + return args; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts index 24bffd9e89f..ffe3b2efff3 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts @@ -1,31 +1,44 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { RedisCommandArguments } from '.'; -import { GeoReplyWith } from './generic-transformers'; -import { transformArguments } from './GEORADIUSBYMEMBER_WITH'; +import GEORADIUSBYMEMBER_WITH from './GEORADIUSBYMEMBER_WITH'; +import { CommandArguments } from '../RESP/types'; +import { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; describe('GEORADIUSBYMEMBER WITH', () => { - it('transformArguments', () => { - const expectedReply: RedisCommandArguments = ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'WITHDIST']; - expectedReply.preserve = ['WITHDIST']; + it('transformArguments', () => { + const expectedReply: CommandArguments = ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'WITHDIST']; + expectedReply.preserve = ['WITHDIST']; - assert.deepEqual( - transformArguments('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]), - expectedReply - ); - }); + assert.deepEqual( + GEORADIUSBYMEMBER_WITH.transformArguments('key', 'member', 3, 'm', [ + GEO_REPLY_WITH.DISTANCE + ]), + expectedReply + ); + }); - testUtils.testWithClient('client.geoRadiusByMemberWith', async client => { - assert.deepEqual( - await client.geoRadiusByMemberWith('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('geoRadiusByMemberWith', async client => { + const [, reply] = await Promise.all([ + client.geoAdd('key', { + member: 'member', + longitude: 1, + latitude: 2 + }), + client.geoRadiusByMemberWith('key', 'member', 1, 'm', [ + GEO_REPLY_WITH.HASH, + GEO_REPLY_WITH.DISTANCE, + GEO_REPLY_WITH.COORDINATES + ]) + ]); - testUtils.testWithCluster('cluster.geoRadiusByMemberWith', async cluster => { - assert.deepEqual( - await cluster.geoRadiusByMemberWith('key', 'member', 3 , 'm', [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + assert.equal(reply.length, 1); + assert.equal(reply[0].member, 'member'); + assert.equal(typeof reply[0].distance, 'string'); + assert.equal(typeof reply[0].hash, 'number'); + assert.equal(typeof reply[0].coordinates!.longitude, 'string'); + assert.equal(typeof reply[0].coordinates!.latitude, 'string'); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts index 7d7dbe06a54..be9472a438f 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts @@ -1,30 +1,36 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoReplyWith, GeoSearchOptions, GeoUnits } from './generic-transformers'; -import { transformArguments as transformGeoRadiusArguments } from './GEORADIUSBYMEMBER'; +import { RedisArgument, CommandArguments, Command } from '../RESP/types'; +import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER'; +import { GeoSearchOptions, GeoUnits, pushGeoSearchOptions } from './GEOSEARCH'; +import GEOSEARCH_WITH, { GeoReplyWith } from './GEOSEARCH_WITH'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUSBYMEMBER'; +export function transformGeoRadiusByMemberWithArguments( + command: RedisArgument, + key: RedisArgument, + from: RedisArgument, + radius: number, + unit: GeoUnits, + replyWith: Array, + options?: GeoSearchOptions +) { + const args: CommandArguments = [ + command, + key, + from, + radius.toString(), + unit + ]; -export function transformArguments( - key: RedisCommandArgument, - member: string, - radius: number, - unit: GeoUnits, - replyWith: Array, - options?: GeoSearchOptions -): RedisCommandArguments { - const args: RedisCommandArguments = transformGeoRadiusArguments( - key, - member, - radius, - unit, - options - ); + pushGeoSearchOptions(args, options); - args.push(...replyWith); + args.push(...replyWith); + args.preserve = replyWith; - args.preserve = replyWith; - - return args; + return args; } -export { transformGeoMembersWithReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: GEORADIUSBYMEMBER.FIRST_KEY_INDEX, + IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, + transformArguments: transformGeoRadiusByMemberWithArguments.bind(undefined, 'GEORADIUSBYMEMBER'), + transformReply: GEOSEARCH_WITH.transformReply +} as const satisfies Command; \ No newline at end of file diff --git a/packages/client/lib/commands/GEORADIUSSTORE.spec.ts b/packages/client/lib/commands/GEORADIUSSTORE.spec.ts deleted file mode 100644 index 4c6372732e5..00000000000 --- a/packages/client/lib/commands/GEORADIUSSTORE.spec.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './GEORADIUSSTORE'; - -describe('GEORADIUSSTORE', () => { - describe('transformArguments', () => { - it('STORE', () => { - assert.deepEqual( - transformArguments('key', {longitude: 1, latitude: 2}, 3 , 'm', 'dest', { - SORT: 'ASC', - COUNT: { - value: 1, - ANY: true - } - }), - ['GEORADIUS', 'key', '1', '2', '3', 'm', 'ASC', 'COUNT', '1', 'ANY', 'STORE', 'dest'] - ); - }); - - it('STOREDIST', () => { - assert.deepEqual( - transformArguments('key', {longitude: 1, latitude: 2}, 3 , 'm', 'dest', { STOREDIST: true }), - ['GEORADIUS', 'key', '1', '2', '3', 'm', 'STOREDIST', 'dest'] - ); - }); - }); - - testUtils.testWithClient('client.geoRadiusStore', async client => { - await client.geoAdd('source', { - longitude: 1, - latitude: 1, - member: 'member' - }); - - assert.equal( - await client.geoRadiusStore('source', {longitude: 1, latitude: 1}, 3 , 'm', 'dest'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.geoRadiusStore', async cluster => { - await cluster.geoAdd('{tag}source', { - longitude: 1, - latitude: 1, - member: 'member' - }); - - assert.equal( - await cluster.geoRadiusStore('{tag}source', {longitude: 1, latitude: 1}, 3 , 'm', '{tag}destination'), - 1 - ); - }, GLOBAL.CLUSTERS.OPEN); -}); diff --git a/packages/client/lib/commands/GEORADIUSSTORE.ts b/packages/client/lib/commands/GEORADIUSSTORE.ts deleted file mode 100644 index ad2317aa3af..00000000000 --- a/packages/client/lib/commands/GEORADIUSSTORE.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoCoordinates, GeoUnits, GeoRadiusStoreOptions, pushGeoRadiusStoreArguments } from './generic-transformers'; - -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUS'; - -export function transformArguments( - key: RedisCommandArgument, - coordinates: GeoCoordinates, - radius: number, - unit: GeoUnits, - destination: RedisCommandArgument, - options?: GeoRadiusStoreOptions, -): RedisCommandArguments { - return pushGeoRadiusStoreArguments( - ['GEORADIUS'], - key, - coordinates, - radius, - unit, - destination, - options - ); -} - -export declare function transformReply(): number; diff --git a/packages/client/lib/commands/GEORADIUS_RO.spec.ts b/packages/client/lib/commands/GEORADIUS_RO.spec.ts index b3cdca18d3f..43a2ef1d583 100644 --- a/packages/client/lib/commands/GEORADIUS_RO.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_RO.spec.ts @@ -1,35 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GEORADIUS_RO'; +import GEORADIUS_RO from './GEORADIUS_RO'; describe('GEORADIUS_RO', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm'), - ['GEORADIUS_RO', 'key', '1', '2', '3', 'm'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + GEORADIUS_RO.transformArguments('key', { + longitude: 1, + latitude: 2 + }, 3, 'm'), + ['GEORADIUS_RO', 'key', '1', '2', '3', 'm'] + ); + }); - testUtils.testWithClient('client.geoRadiusRo', async client => { - assert.deepEqual( - await client.geoRadiusRo('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm'), - [] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.geoRadiusRo', async cluster => { - assert.deepEqual( - await cluster.geoRadiusRo('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm'), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('geoRadiusRo', async client => { + assert.deepEqual( + await client.geoRadiusRo('key', { + longitude: 1, + latitude: 2 + }, 3, 'm'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEORADIUS_RO.ts b/packages/client/lib/commands/GEORADIUS_RO.ts index ac378a5150b..be8bb4b530d 100644 --- a/packages/client/lib/commands/GEORADIUS_RO.ts +++ b/packages/client/lib/commands/GEORADIUS_RO.ts @@ -1,25 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchOptions, GeoCoordinates, pushGeoRadiusArguments, GeoUnits } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - coordinates: GeoCoordinates, - radius: number, - unit: GeoUnits, - options?: GeoSearchOptions -): RedisCommandArguments { - return pushGeoRadiusArguments( - ['GEORADIUS_RO'], - key, - coordinates, - radius, - unit, - options - ); -} - -export declare function transformReply(): Array; +import { Command } from '../RESP/types'; +import GEORADIUS, { transformGeoRadiusArguments } from './GEORADIUS'; + +export default { + FIRST_KEY_INDEX: GEORADIUS.FIRST_KEY_INDEX, + IS_READ_ONLY: true, + transformArguments: transformGeoRadiusArguments.bind(undefined, 'GEORADIUS_RO'), + transformReply: GEORADIUS.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts b/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts index 21b00ff90b8..cb0540d8a18 100644 --- a/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts @@ -1,40 +1,48 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { RedisCommandArguments } from '.'; -import { GeoReplyWith } from './generic-transformers'; -import { transformArguments } from './GEORADIUS_RO_WITH'; +import GEORADIUS_RO_WITH from './GEORADIUS_RO_WITH'; +import { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; +import { CommandArguments } from '../RESP/types'; describe('GEORADIUS_RO WITH', () => { - it('transformArguments', () => { - const expectedReply: RedisCommandArguments = ['GEORADIUS_RO', 'key', '1', '2', '3', 'm', 'WITHDIST']; - expectedReply.preserve = ['WITHDIST']; + it('transformArguments', () => { + const expectedReply: CommandArguments = ['GEORADIUS_RO', 'key', '1', '2', '3', 'm', 'WITHDIST']; + expectedReply.preserve = ['WITHDIST']; - assert.deepEqual( - transformArguments('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm', [GeoReplyWith.DISTANCE]), - expectedReply - ); - }); + assert.deepEqual( + GEORADIUS_RO_WITH.transformArguments('key', { + longitude: 1, + latitude: 2 + }, 3, 'm', [GEO_REPLY_WITH.DISTANCE]), + expectedReply + ); + }); - testUtils.testWithClient('client.geoRadiusRoWith', async client => { - assert.deepEqual( - await client.geoRadiusRoWith('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm', [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('geoRadiusRoWith', async client => { + const [, reply] = await Promise.all([ + client.geoAdd('key', { + member: 'member', + longitude: 1, + latitude: 2 + }), + client.geoRadiusRoWith('key', { + longitude: 1, + latitude: 2 + }, 1, 'm', [ + GEO_REPLY_WITH.HASH, + GEO_REPLY_WITH.DISTANCE, + GEO_REPLY_WITH.COORDINATES + ]) + ]); - testUtils.testWithCluster('cluster.geoRadiusReadOnlyWith', async cluster => { - assert.deepEqual( - await cluster.geoRadiusRoWith('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm', [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + assert.equal(reply.length, 1); + assert.equal(reply[0].member, 'member'); + assert.equal(typeof reply[0].distance, 'string'); + assert.equal(typeof reply[0].hash, 'number'); + assert.equal(typeof reply[0].coordinates!.longitude, 'string'); + assert.equal(typeof reply[0].coordinates!.latitude, 'string'); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts index 424e5fcd998..37cf594ce9d 100644 --- a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts @@ -1,30 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoReplyWith, GeoSearchOptions, GeoCoordinates, GeoUnits } from './generic-transformers'; -import { transformArguments as transformGeoRadiusRoArguments } from './GEORADIUS_RO'; - -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUS_RO'; - -export function transformArguments( - key: RedisCommandArgument, - coordinates: GeoCoordinates, - radius: number, - unit: GeoUnits, - replyWith: Array, - options?: GeoSearchOptions -): RedisCommandArguments { - const args: RedisCommandArguments = transformGeoRadiusRoArguments( - key, - coordinates, - radius, - unit, - options - ); - - args.push(...replyWith); - - args.preserve = replyWith; - - return args; -} - -export { transformGeoMembersWithReply as transformReply } from './generic-transformers'; +import { Command } from '../RESP/types'; +import GEORADIUS_WITH, { transformGeoRadiusWithArguments } from './GEORADIUS_WITH'; + +export default { + FIRST_KEY_INDEX: GEORADIUS_WITH.FIRST_KEY_INDEX, + IS_READ_ONLY: true, + transformArguments: transformGeoRadiusWithArguments.bind(undefined, 'GEORADIUS_RO'), + transformReply: GEORADIUS_WITH.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUS_STORE.spec.ts b/packages/client/lib/commands/GEORADIUS_STORE.spec.ts new file mode 100644 index 00000000000..04a7d28aa95 --- /dev/null +++ b/packages/client/lib/commands/GEORADIUS_STORE.spec.ts @@ -0,0 +1,48 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import GEORADIUS_STORE from './GEORADIUS_STORE'; + +describe('GEORADIUS STORE', () => { + describe('transformArguments', () => { + it('STORE', () => { + assert.deepEqual( + GEORADIUS_STORE.transformArguments('key', { + longitude: 1, + latitude: 2 + }, 3, 'm', 'destination'), + ['GEORADIUS', 'key', '1', '2', '3', 'm', 'STORE', 'destination'] + ); + }); + + it('STOREDIST', () => { + assert.deepEqual( + GEORADIUS_STORE.transformArguments('key', { + longitude: 1, + latitude: 2 + }, 3, 'm', 'destination', { + STOREDIST: true + }), + ['GEORADIUS', 'key', '1', '2', '3', 'm', 'STOREDIST', 'destination'] + ); + }); + }); + + testUtils.testAll('geoRadiusStore', async client => { + const [, reply] = await Promise.all([ + client.geoAdd('{tag}source', { + longitude: 1, + latitude: 2, + member: 'member' + }), + client.geoRadiusStore('{tag}source', { + longitude: 1, + latitude: 2 + }, 1, 'm', '{tag}destination') + ]); + + assert.equal(reply, 1); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/GEORADIUS_STORE.ts b/packages/client/lib/commands/GEORADIUS_STORE.ts new file mode 100644 index 00000000000..3a553ebf8be --- /dev/null +++ b/packages/client/lib/commands/GEORADIUS_STORE.ts @@ -0,0 +1,31 @@ +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import GEORADIUS, { transformGeoRadiusArguments } from './GEORADIUS'; +import { GeoCoordinates, GeoSearchOptions, GeoUnits } from './GEOSEARCH'; + +export interface GeoRadiusStoreOptions extends GeoSearchOptions { + STOREDIST?: boolean; +} + +export default { + FIRST_KEY_INDEX: GEORADIUS.FIRST_KEY_INDEX, + IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, + transformArguments( + key: RedisArgument, + from: GeoCoordinates, + radius: number, + unit: GeoUnits, + destination: RedisArgument, + options?: GeoRadiusStoreOptions + ) { + const args = transformGeoRadiusArguments('GEORADIUS', key, from, radius, unit, options); + + if (options?.STOREDIST) { + args.push('STOREDIST', destination); + } else { + args.push('STORE', destination); + } + + return args; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUS_WITH.spec.ts b/packages/client/lib/commands/GEORADIUS_WITH.spec.ts index 44366198beb..bdbfc9c1f3a 100644 --- a/packages/client/lib/commands/GEORADIUS_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_WITH.spec.ts @@ -1,40 +1,48 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { RedisCommandArguments } from '.'; -import { GeoReplyWith } from './generic-transformers'; -import { transformArguments } from './GEORADIUS_WITH'; +import GEORADIUS_WITH from './GEORADIUS_WITH'; +import { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; +import { CommandArguments } from '../RESP/types'; describe('GEORADIUS WITH', () => { - it('transformArguments', () => { - const expectedReply: RedisCommandArguments = ['GEORADIUS', 'key', '1', '2', '3', 'm', 'WITHDIST']; - expectedReply.preserve = ['WITHDIST']; + it('transformArguments', () => { + const expectedReply: CommandArguments = ['GEORADIUS', 'key', '1', '2', '3', 'm', 'WITHDIST']; + expectedReply.preserve = ['WITHDIST']; - assert.deepEqual( - transformArguments('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm', [GeoReplyWith.DISTANCE]), - expectedReply - ); - }); + assert.deepEqual( + GEORADIUS_WITH.transformArguments('key', { + longitude: 1, + latitude: 2 + }, 3, 'm', [GEO_REPLY_WITH.DISTANCE]), + expectedReply + ); + }); - testUtils.testWithClient('client.geoRadiusWith', async client => { - assert.deepEqual( - await client.geoRadiusWith('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm', [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('geoRadiusWith', async client => { + const [, reply] = await Promise.all([ + client.geoAdd('key', { + member: 'member', + longitude: 1, + latitude: 2 + }), + client.geoRadiusWith('key', { + longitude: 1, + latitude: 2 + }, 1, 'm', [ + GEO_REPLY_WITH.HASH, + GEO_REPLY_WITH.DISTANCE, + GEO_REPLY_WITH.COORDINATES + ]) + ]); - testUtils.testWithCluster('cluster.geoRadiusWith', async cluster => { - assert.deepEqual( - await cluster.geoRadiusWith('key', { - longitude: 1, - latitude: 2 - }, 3 , 'm', [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + assert.equal(reply.length, 1); + assert.equal(reply[0].member, 'member'); + assert.equal(typeof reply[0].distance, 'string'); + assert.equal(typeof reply[0].hash, 'number'); + assert.equal(typeof reply[0].coordinates?.longitude, 'string'); + assert.equal(typeof reply[0].coordinates?.latitude, 'string'); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEORADIUS_WITH.ts b/packages/client/lib/commands/GEORADIUS_WITH.ts index dc3f4288f01..d72d8d49322 100644 --- a/packages/client/lib/commands/GEORADIUS_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_WITH.ts @@ -1,30 +1,33 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoReplyWith, GeoSearchOptions, GeoCoordinates, GeoUnits } from './generic-transformers'; -import { transformArguments as transformGeoRadiusArguments } from './GEORADIUS'; +import { CommandArguments, Command, RedisArgument } from '../RESP/types'; +import GEORADIUS, { transformGeoRadiusArguments } from './GEORADIUS'; +import { GeoCoordinates, GeoSearchOptions, GeoUnits } from './GEOSEARCH'; +import GEOSEARCH_WITH, { GeoReplyWith } from './GEOSEARCH_WITH'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEORADIUS'; - -export function transformArguments( - key: RedisCommandArgument, - coordinates: GeoCoordinates, - radius: number, - unit: GeoUnits, - replyWith: Array, - options?: GeoSearchOptions -): RedisCommandArguments { - const args: RedisCommandArguments = transformGeoRadiusArguments( - key, - coordinates, - radius, - unit, - options - ); - - args.push(...replyWith); - - args.preserve = replyWith; - - return args; +export function transformGeoRadiusWithArguments( + command: RedisArgument, + key: RedisArgument, + from: GeoCoordinates, + radius: number, + unit: GeoUnits, + replyWith: Array, + options?: GeoSearchOptions +) { + const args: CommandArguments = transformGeoRadiusArguments( + command, + key, + from, + radius, + unit, + options + ); + args.push(...replyWith); + args.preserve = replyWith; + return args; } -export { transformGeoMembersWithReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: GEORADIUS.FIRST_KEY_INDEX, + IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, + transformArguments: transformGeoRadiusWithArguments.bind(undefined, 'GEORADIUS'), + transformReply: GEOSEARCH_WITH.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEOSEARCH.spec.ts b/packages/client/lib/commands/GEOSEARCH.spec.ts index ec0d4bcc4f8..49f076880a6 100644 --- a/packages/client/lib/commands/GEOSEARCH.spec.ts +++ b/packages/client/lib/commands/GEOSEARCH.spec.ts @@ -1,37 +1,87 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GEOSEARCH'; +import GEOSEARCH from './GEOSEARCH'; describe('GEOSEARCH', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member', { - radius: 1, - unit: 'm' - }), - ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm'] - ); + describe('transformArguments', () => { + it('FROMMEMBER, BYRADIUS, without options', () => { + assert.deepEqual( + GEOSEARCH.transformArguments('key', 'member', { + radius: 1, + unit: 'm' + }), + ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm'] + ); + }); + + it('FROMLONLAT, BYBOX, without options', () => { + assert.deepEqual( + GEOSEARCH.transformArguments('key', { + longitude: 1, + latitude: 2 + }, { + width: 1, + height: 2, + unit: 'm' + }), + ['GEOSEARCH', 'key', 'FROMLONLAT', '1', '2', 'BYBOX', '1', '2', 'm'] + ); }); - testUtils.testWithClient('client.geoSearch', async client => { + it('with SORT', () => { + assert.deepEqual( + GEOSEARCH.transformArguments('key', 'member', { + radius: 1, + unit: 'm' + }, { + SORT: 'ASC' + }), + ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'ASC'] + ); + }); + + describe('with COUNT', () => { + it('number', () => { assert.deepEqual( - await client.geoSearch('key', 'member', { - radius: 1, - unit: 'm' - }), - [] + GEOSEARCH.transformArguments('key', 'member', { + radius: 1, + unit: 'm' + }, { + COUNT: 1 + }), + ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'COUNT', '1'] ); - }, GLOBAL.SERVERS.OPEN); + }); - testUtils.testWithCluster('cluster.geoSearch', async cluster => { + it('with ANY', () => { assert.deepEqual( - await cluster.geoSearch('key', 'member', { - radius: 1, - unit: 'm' - }), - [] + GEOSEARCH.transformArguments('key', 'member', { + radius: 1, + unit: 'm' + }, { + COUNT: { + value: 1, + ANY: true + } + }), + ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'COUNT', '1', 'ANY'] ); - }, GLOBAL.CLUSTERS.OPEN); + }); + }); + }); + + testUtils.testAll('geoSearch', async client => { + assert.deepEqual( + await client.geoSearch('key', 'member', { + radius: 1, + unit: 'm' + }), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEOSEARCH.ts b/packages/client/lib/commands/GEOSEARCH.ts index a02a21391f6..c4deaa37e6c 100644 --- a/packages/client/lib/commands/GEOSEARCH.ts +++ b/packages/client/lib/commands/GEOSEARCH.ts @@ -1,17 +1,94 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments } from './generic-transformers'; +import { RedisArgument, CommandArguments, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; +export type GeoUnits = 'm' | 'km' | 'mi' | 'ft'; -export const IS_READ_ONLY = true; +export interface GeoCoordinates { + longitude: RedisArgument | number; + latitude: RedisArgument | number; +} + +export type GeoSearchFrom = RedisArgument | GeoCoordinates; + +export interface GeoSearchByRadius { + radius: number; + unit: GeoUnits; +} + +export interface GeoSearchByBox { + width: number; + height: number; + unit: GeoUnits; +} + +export type GeoSearchBy = GeoSearchByRadius | GeoSearchByBox; + +export function pushGeoSearchArguments( + args: CommandArguments, + key: RedisArgument, + from: GeoSearchFrom, + by: GeoSearchBy, + options?: GeoSearchOptions +) { + args.push(key); + + if (typeof from === 'string' || from instanceof Buffer) { + args.push('FROMMEMBER', from); + } else { + args.push('FROMLONLAT', from.longitude.toString(), from.latitude.toString()); + } + + if ('radius' in by) { + args.push('BYRADIUS', by.radius.toString(), by.unit); + } else { + args.push('BYBOX', by.width.toString(), by.height.toString(), by.unit); + } + + pushGeoSearchOptions(args, options); -export function transformArguments( - key: RedisCommandArgument, + return args; +} + +export type GeoCountArgument = number | { + value: number; + ANY?: boolean; +}; + +export interface GeoSearchOptions { + SORT?: 'ASC' | 'DESC'; + COUNT?: GeoCountArgument; +} + +export function pushGeoSearchOptions( + args: CommandArguments, + options?: GeoSearchOptions +) { + if (options?.SORT) { + args.push(options.SORT); + } + + if (options?.COUNT) { + if (typeof options.COUNT === 'number') { + args.push('COUNT', options.COUNT.toString()); + } else { + args.push('COUNT', options.COUNT.value.toString()); + + if (options.COUNT.ANY) { + args.push('ANY'); + } + } + } +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, options?: GeoSearchOptions -): RedisCommandArguments { + ) { return pushGeoSearchArguments(['GEOSEARCH'], key, from, by, options); -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts b/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts index eb32fa134e4..c66d3e8e45e 100644 --- a/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts +++ b/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts @@ -1,81 +1,44 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './GEOSEARCHSTORE'; +import GEOSEARCHSTORE from './GEOSEARCHSTORE'; describe('GEOSEARCHSTORE', () => { - testUtils.isVersionGreaterThanHook([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'] - ); - }); + testUtils.isVersionGreaterThanHook([6, 2]); + + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + GEOSEARCHSTORE.transformArguments('source', 'destination', 'member', { + radius: 1, + unit: 'm' + }), + ['GEOSEARCHSTORE', 'source', 'destination', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm'] + ); }); - it('transformReply with empty array (https://github.com/redis/redis/issues/9261)', () => { - assert.throws( - () => (transformReply as any)([]), - TypeError - ); + it('with STOREDIST', () => { + assert.deepEqual( + GEOSEARCHSTORE.transformArguments('destination', 'source', 'member', { + radius: 1, + unit: 'm' + }, { + STOREDIST: true + }), + ['GEOSEARCHSTORE', 'destination', 'source', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'STOREDIST'] + ); }); - - testUtils.testWithClient('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 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('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 - ); - }, GLOBAL.CLUSTERS.OPEN); + }); + + testUtils.testAll('geoSearchStore', async client => { + assert.equal( + await client.geoSearchStore('{tag}destination', '{tag}source', 'member', { + radius: 1, + unit: 'm' + }), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEOSEARCHSTORE.ts b/packages/client/lib/commands/GEOSEARCHSTORE.ts index 7a91450cd9e..15635560217 100644 --- a/packages/client/lib/commands/GEOSEARCHSTORE.ts +++ b/packages/client/lib/commands/GEOSEARCHSTORE.ts @@ -1,38 +1,27 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments } from './GEOSEARCH'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEOSEARCH'; - -interface GeoSearchStoreOptions extends GeoSearchOptions { - STOREDIST?: true; +export interface GeoSearchStoreOptions extends GeoSearchOptions { + STOREDIST?: boolean; } -export function transformArguments( - destination: RedisCommandArgument, - source: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + destination: RedisArgument, + source: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, options?: GeoSearchStoreOptions -): RedisCommandArguments { - const args = pushGeoSearchArguments( - ['GEOSEARCHSTORE', destination], - source, - from, - by, - options - ); + ) { + const args = pushGeoSearchArguments(['GEOSEARCHSTORE', destination], source, from, by, options); if (options?.STOREDIST) { - args.push('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; -} + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts b/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts index c1f5213775a..e27fb295aaf 100644 --- a/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts +++ b/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts @@ -1,42 +1,49 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { RedisCommandArguments } from '.'; -import { GeoReplyWith } from './generic-transformers'; -import { transformArguments } from './GEOSEARCH_WITH'; +import GEOSEARCH_WITH, { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; +import { CommandArguments } from '../RESP/types'; describe('GEOSEARCH WITH', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - const expectedReply: RedisCommandArguments = ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'WITHDIST']; - expectedReply.preserve = ['WITHDIST']; + it('transformArguments', () => { + const expectedReply: CommandArguments = ['GEOSEARCH', 'key', 'FROMMEMBER', 'member', 'BYRADIUS', '1', 'm', 'WITHDIST']; + expectedReply.preserve = ['WITHDIST']; - assert.deepEqual( - transformArguments('key', 'member', { - radius: 1, - unit: 'm' - }, [GeoReplyWith.DISTANCE]), - expectedReply - ); - }); + assert.deepEqual( + GEOSEARCH_WITH.transformArguments('key', 'member', { + radius: 1, + unit: 'm' + }, [GEO_REPLY_WITH.DISTANCE]), + expectedReply + ); + }); - testUtils.testWithClient('client.geoSearchWith', async client => { - assert.deepEqual( - await client.geoSearchWith('key', 'member', { - radius: 1, - unit: 'm' - }, [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('.geoSearchWith', async client => { + const [ , reply ] = await Promise.all([ + client.geoAdd('key', { + member: 'member', + longitude: 1, + latitude: 2 + }), + client.geoSearchWith('key', 'member', { + radius: 1, + unit: 'm' + }, [ + GEO_REPLY_WITH.HASH, + GEO_REPLY_WITH.DISTANCE, + GEO_REPLY_WITH.COORDINATES + ]) + ]); - testUtils.testWithCluster('cluster.geoSearchWith', async cluster => { - assert.deepEqual( - await cluster.geoSearchWith('key', 'member', { - radius: 1, - unit: 'm' - }, [GeoReplyWith.DISTANCE]), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + assert.equal(reply.length, 1); + assert.equal(reply[0].member, 'member'); + assert.equal(typeof reply[0].distance, 'string'); + assert.equal(typeof reply[0].hash, 'number'); + assert.equal(typeof reply[0].coordinates!.longitude, 'string'); + assert.equal(typeof reply[0].coordinates!.latitude, 'string'); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GEOSEARCH_WITH.ts b/packages/client/lib/commands/GEOSEARCH_WITH.ts index d7a5f456a94..19088230f0f 100644 --- a/packages/client/lib/commands/GEOSEARCH_WITH.ts +++ b/packages/client/lib/commands/GEOSEARCH_WITH.ts @@ -1,23 +1,73 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { GeoSearchFrom, GeoSearchBy, GeoReplyWith, GeoSearchOptions } from './generic-transformers'; -import { transformArguments as geoSearchTransformArguments } from './GEOSEARCH'; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Command } from '../RESP/types'; +import GEOSEARCH, { GeoSearchBy, GeoSearchFrom, GeoSearchOptions } from './GEOSEARCH'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './GEOSEARCH'; +export const GEO_REPLY_WITH = { + DISTANCE: 'WITHDIST', + HASH: 'WITHHASH', + COORDINATES: 'WITHCOORD' +} as const; -export function transformArguments( - key: RedisCommandArgument, +export type GeoReplyWith = typeof GEO_REPLY_WITH[keyof typeof GEO_REPLY_WITH]; + +export interface GeoReplyWithMember { + member: BlobStringReply; + distance?: BlobStringReply; + hash?: NumberReply; + coordinates?: { + longitude: DoubleReply; + latitude: DoubleReply; + }; +} + +export default { + FIRST_KEY_INDEX: GEOSEARCH.FIRST_KEY_INDEX, + IS_READ_ONLY: GEOSEARCH.IS_READ_ONLY, + transformArguments( + key: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, replyWith: Array, options?: GeoSearchOptions -): RedisCommandArguments { - const args: RedisCommandArguments = geoSearchTransformArguments(key, from, by, options); - + ) { + const args = GEOSEARCH.transformArguments(key, from, by, options); args.push(...replyWith); - args.preserve = replyWith; - return args; -} + }, + transformReply( + reply: UnwrapReply]>>>, + replyWith: Array + ) { + const replyWithSet = new Set(replyWith); + let index = 0; + const distanceIndex = replyWithSet.has(GEO_REPLY_WITH.DISTANCE) && ++index, + hashIndex = replyWithSet.has(GEO_REPLY_WITH.HASH) && ++index, + coordinatesIndex = replyWithSet.has(GEO_REPLY_WITH.COORDINATES) && ++index; + + return reply.map(raw => { + const unwrapped = raw as unknown as UnwrapReply; + + const item: GeoReplyWithMember = { + member: unwrapped[0] + }; + + if (distanceIndex) { + item.distance = unwrapped[distanceIndex]; + } + + if (hashIndex) { + item.hash = unwrapped[hashIndex]; + } + + if (coordinatesIndex) { + const [longitude, latitude] = unwrapped[coordinatesIndex]; + item.coordinates = { + longitude, + latitude + }; + } -export { transformGeoMembersWithReply as transformReply } from './generic-transformers'; + return item; + }); + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/GET.spec.ts b/packages/client/lib/commands/GET.spec.ts index 2946ea19b60..4bd74183222 100644 --- a/packages/client/lib/commands/GET.spec.ts +++ b/packages/client/lib/commands/GET.spec.ts @@ -1,33 +1,22 @@ -import { strict as assert } from 'assert'; -import RedisClient from '../client'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GET'; +import GET from './GET'; describe('GET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['GET', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + GET.transformArguments('key'), + ['GET', 'key'] + ); + }); - testUtils.testWithClient('client.get', async client => { - const a = await client.get( - 'key' - ); - - - - assert.equal( - await client.get('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.get', async cluster => { - assert.equal( - await cluster.get('key'), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('get', async client => { + assert.equal( + await client.get('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GET.ts b/packages/client/lib/commands/GET.ts index 127b0a56349..bb3db4f76d9 100644 --- a/packages/client/lib/commands/GET.ts +++ b/packages/client/lib/commands/GET.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['GET', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GETBIT.spec.ts b/packages/client/lib/commands/GETBIT.spec.ts index 4206084eced..ac39222b918 100644 --- a/packages/client/lib/commands/GETBIT.spec.ts +++ b/packages/client/lib/commands/GETBIT.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GETBIT'; +import GETBIT from './GETBIT'; describe('GETBIT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0), - ['GETBIT', 'key', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + GETBIT.transformArguments('key', 0), + ['GETBIT', 'key', '0'] + ); + }); - testUtils.testWithClient('client.getBit', async client => { - assert.equal( - await client.getBit('key', 0), - 0 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.getBit', async cluster => { - assert.equal( - await cluster.getBit('key', 0), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('getBit', async client => { + assert.equal( + await client.getBit('key', 0), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GETBIT.ts b/packages/client/lib/commands/GETBIT.ts index 67f67f39b19..d8ece8f523a 100644 --- a/packages/client/lib/commands/GETBIT.ts +++ b/packages/client/lib/commands/GETBIT.ts @@ -1,15 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; import { BitValue } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - offset: number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, offset: number) { return ['GETBIT', key, offset.toString()]; -} - -export declare function transformReply(): BitValue; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GETDEL.spec.ts b/packages/client/lib/commands/GETDEL.spec.ts index db3a486696a..311f15e554d 100644 --- a/packages/client/lib/commands/GETDEL.spec.ts +++ b/packages/client/lib/commands/GETDEL.spec.ts @@ -1,28 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GETDEL'; +import GETDEL from './GETDEL'; describe('GETDEL', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['GETDEL', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + GETDEL.transformArguments('key'), + ['GETDEL', 'key'] + ); + }); - testUtils.testWithClient('client.getDel', async client => { - assert.equal( - await client.getDel('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.getDel', async cluster => { - assert.equal( - await cluster.getDel('key'), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('getDel', async client => { + assert.equal( + await client.getDel('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GETDEL.ts b/packages/client/lib/commands/GETDEL.ts index 2d91e6cc025..c11fd047df4 100644 --- a/packages/client/lib/commands/GETDEL.ts +++ b/packages/client/lib/commands/GETDEL.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['GETDEL', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GETEX.spec.ts b/packages/client/lib/commands/GETEX.spec.ts index 1bf86089da1..302d034b961 100644 --- a/packages/client/lib/commands/GETEX.spec.ts +++ b/packages/client/lib/commands/GETEX.spec.ts @@ -1,96 +1,122 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GETEX'; +import GETEX from './GETEX'; describe('GETEX', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - describe('transformArguments', () => { - it('EX', () => { - assert.deepEqual( - transformArguments('key', { - EX: 1 - }), - ['GETEX', 'key', 'EX', '1'] - ); - }); + describe('transformArguments', () => { + it('EX | PX', () => { + assert.deepEqual( + GETEX.transformArguments('key', { + type: 'EX', + value: 1 + }), + ['GETEX', 'key', 'EX', '1'] + ); + }); - it('PX', () => { - assert.deepEqual( - transformArguments('key', { - PX: 1 - }), - ['GETEX', 'key', 'PX', '1'] - ); - }); + it('EX (backwards compatibility)', () => { + assert.deepEqual( + GETEX.transformArguments('key', { + EX: 1 + }), + ['GETEX', 'key', 'EX', '1'] + ); + }); - describe('EXAT', () => { - it('number', () => { - assert.deepEqual( - transformArguments('key', { - EXAT: 1 - }), - ['GETEX', 'key', 'EXAT', '1'] - ); - }); + it('PX (backwards compatibility)', () => { + assert.deepEqual( + GETEX.transformArguments('key', { + PX: 1 + }), + ['GETEX', 'key', 'PX', '1'] + ); + }); - it('date', () => { - const d = new Date(); - assert.deepEqual( - transformArguments('key', { - EXAT: d - }), - ['GETEX', 'key', 'EXAT', Math.floor(d.getTime() / 1000).toString()] - ); - }); - }); + describe('EXAT | PXAT', () => { + it('number', () => { + assert.deepEqual( + GETEX.transformArguments('key', { + type: 'EXAT', + value: 1 + }), + ['GETEX', 'key', 'EXAT', '1'] + ); + }); - describe('PXAT', () => { - it('number', () => { - assert.deepEqual( - transformArguments('key', { - PXAT: 1 - }), - ['GETEX', 'key', 'PXAT', '1'] - ); - }); + it('date', () => { + const d = new Date(); + assert.deepEqual( + GETEX.transformArguments('key', { + EXAT: d + }), + ['GETEX', 'key', 'EXAT', Math.floor(d.getTime() / 1000).toString()] + ); + }); + }); - it('date', () => { - const d = new Date(); - assert.deepEqual( - transformArguments('key', { - PXAT: d - }), - ['GETEX', 'key', 'PXAT', d.getTime().toString()] - ); - }); - }); + describe('EXAT (backwards compatibility)', () => { + it('number', () => { + assert.deepEqual( + GETEX.transformArguments('key', { + EXAT: 1 + }), + ['GETEX', 'key', 'EXAT', '1'] + ); + }); - it('PERSIST', () => { - assert.deepEqual( - transformArguments('key', { - PERSIST: true - }), - ['GETEX', 'key', 'PERSIST'] - ); - }); + it('date', () => { + const d = new Date(); + assert.deepEqual( + GETEX.transformArguments('key', { + EXAT: d + }), + ['GETEX', 'key', 'EXAT', Math.floor(d.getTime() / 1000).toString()] + ); + }); }); - testUtils.testWithClient('client.getEx', async client => { - assert.equal( - await client.getEx('key', { - PERSIST: true - }), - null + describe('PXAT (backwards compatibility)', () => { + it('number', () => { + assert.deepEqual( + GETEX.transformArguments('key', { + PXAT: 1 + }), + ['GETEX', 'key', 'PXAT', '1'] ); - }, GLOBAL.SERVERS.OPEN); + }); - testUtils.testWithCluster('cluster.getEx', async cluster => { - assert.equal( - await cluster.getEx('key', { - PERSIST: true - }), - null + it('date', () => { + const d = new Date(); + assert.deepEqual( + GETEX.transformArguments('key', { + PXAT: d + }), + ['GETEX', 'key', 'PXAT', d.getTime().toString()] ); - }, GLOBAL.CLUSTERS.OPEN); + }); + }); + + it('PERSIST (backwards compatibility)', () => { + assert.deepEqual( + GETEX.transformArguments('key', { + PERSIST: true + }), + ['GETEX', 'key', 'PERSIST'] + ); + }); + }); + + testUtils.testAll('getEx', async client => { + assert.equal( + await client.getEx('key', { + type: 'PERSIST' + }), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GETEX.ts b/packages/client/lib/commands/GETEX.ts index 5b3cec6d887..8244350eddb 100644 --- a/packages/client/lib/commands/GETEX.ts +++ b/packages/client/lib/commands/GETEX.ts @@ -1,39 +1,78 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; import { transformEXAT, transformPXAT } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -type GetExModes = { - EX: number; +export type GetExOptions = { + type: 'EX' | 'PX'; + value: number; +} | { + type: 'EXAT' | 'PXAT'; + value: number | Date; +} | { + type: 'PERSIST'; +} | { + /** + * @deprecated Use `{ type: 'EX', value: number }` instead. + */ + EX: number; } | { - PX: number; + /** + * @deprecated Use `{ type: 'PX', value: number }` instead. + */ + PX: number; } | { - EXAT: number | Date; + /** + * @deprecated Use `{ type: 'EXAT', value: number | Date }` instead. + */ + EXAT: number | Date; } | { - PXAT: number | Date; + /** + * @deprecated Use `{ type: 'PXAT', value: number | Date }` instead. + */ + PXAT: number | Date; } | { - PERSIST: true; + /** + * @deprecated Use `{ type: 'PERSIST' }` instead. + */ + PERSIST: true; }; -export function transformArguments( - key: RedisCommandArgument, - mode: GetExModes -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, options: GetExOptions) { 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 + if ('type' in options) { + switch (options.type) { + case 'EX': + case 'PX': + args.push(options.type, options.value.toString()); + break; + + case 'EXAT': + case 'PXAT': + args.push(options.type, transformEXAT(options.value)); + break; + + case 'PERSIST': + args.push('PERSIST'); + break; + } + } else { + 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', transformEXAT(options.EXAT)); + } else if ('PXAT' in options) { + args.push('PXAT', transformPXAT(options.PXAT)); + } else { // PERSIST args.push('PERSIST'); + } } - + return args; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GETRANGE.spec.ts b/packages/client/lib/commands/GETRANGE.spec.ts index 0c9dbc2c70f..2aac1ca16d9 100644 --- a/packages/client/lib/commands/GETRANGE.spec.ts +++ b/packages/client/lib/commands/GETRANGE.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GETRANGE'; +import GETRANGE from './GETRANGE'; describe('GETRANGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, -1), - ['GETRANGE', 'key', '0', '-1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + GETRANGE.transformArguments('key', 0, -1), + ['GETRANGE', 'key', '0', '-1'] + ); + }); - testUtils.testWithClient('client.getRange', async client => { - assert.equal( - await client.getRange('key', 0, -1), - '' - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lTrim', async cluster => { - assert.equal( - await cluster.getRange('key', 0, -1), - '' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('getRange', async client => { + assert.equal( + await client.getRange('key', 0, -1), + '' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GETRANGE.ts b/packages/client/lib/commands/GETRANGE.ts index 2d12d937cc6..e5357cd120b 100644 --- a/packages/client/lib/commands/GETRANGE.ts +++ b/packages/client/lib/commands/GETRANGE.ts @@ -1,15 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - start: number, - end: number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, start: number, end: number) { return ['GETRANGE', key, start.toString(), end.toString()]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/GETSET.spec.ts b/packages/client/lib/commands/GETSET.spec.ts index 73fbcec57ea..6583ec34f75 100644 --- a/packages/client/lib/commands/GETSET.spec.ts +++ b/packages/client/lib/commands/GETSET.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GETSET'; +import GETSET from './GETSET'; describe('GETSET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'value'), - ['GETSET', 'key', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + GETSET.transformArguments('key', 'value'), + ['GETSET', 'key', 'value'] + ); + }); - testUtils.testWithClient('client.getSet', async client => { - assert.equal( - await client.getSet('key', 'value'), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.getSet', async cluster => { - assert.equal( - await cluster.getSet('key', 'value'), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('getSet', async client => { + assert.equal( + await client.getSet('key', 'value'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/GETSET.ts b/packages/client/lib/commands/GETSET.ts index 87d111792c6..bbe920181b2 100644 --- a/packages/client/lib/commands/GETSET.ts +++ b/packages/client/lib/commands/GETSET.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - value: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, value: RedisArgument) { return ['GETSET', key, value]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HDEL.spec.ts b/packages/client/lib/commands/HDEL.spec.ts index eb24bcfacbd..9f69485d9fe 100644 --- a/packages/client/lib/commands/HDEL.spec.ts +++ b/packages/client/lib/commands/HDEL.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HDEL'; +import HDEL from './HDEL'; describe('HDEL', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HDEL', 'key', 'field'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + HDEL.transformArguments('key', 'field'), + ['HDEL', 'key', 'field'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['HDEL', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + HDEL.transformArguments('key', ['1', '2']), + ['HDEL', 'key', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.hDel', async client => { - assert.equal( - await client.hDel('key', 'field'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hDel', async client => { + assert.equal( + await client.hDel('key', 'field'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HDEL.ts b/packages/client/lib/commands/HDEL.ts index 1a994e109d6..64aa55edda6 100644 --- a/packages/client/lib/commands/HDEL.ts +++ b/packages/client/lib/commands/HDEL.ts @@ -1,13 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['HDEL', key], field); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, field: RedisVariadicArgument) { + return pushVariadicArguments(['HDEL', key], field); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HELLO.spec.ts b/packages/client/lib/commands/HELLO.spec.ts index 12d6d98c7c9..f7f117f18c7 100644 --- a/packages/client/lib/commands/HELLO.spec.ts +++ b/packages/client/lib/commands/HELLO.spec.ts @@ -1,76 +1,71 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HELLO'; +import HELLO from './HELLO'; describe('HELLO', () => { - testUtils.isVersionGreaterThanHook([6]); + testUtils.isVersionGreaterThanHook([6]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['HELLO'] - ); - }); - - it('with protover', () => { - assert.deepEqual( - transformArguments({ - protover: 3 - }), - ['HELLO', '3'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + HELLO.transformArguments(), + ['HELLO'] + ); + }); - it('with protover, auth', () => { - assert.deepEqual( - transformArguments({ - protover: 3, - auth: { - username: 'username', - password: 'password' - } - }), - ['HELLO', '3', 'AUTH', 'username', 'password'] - ); - }); + it('with protover', () => { + assert.deepEqual( + HELLO.transformArguments(3), + ['HELLO', '3'] + ); + }); - it('with protover, clientName', () => { - assert.deepEqual( - transformArguments({ - protover: 3, - clientName: 'clientName' - }), - ['HELLO', '3', 'SETNAME', 'clientName'] - ); - }); + it('with protover, AUTH', () => { + assert.deepEqual( + HELLO.transformArguments(3, { + AUTH: { + username: 'username', + password: 'password' + } + }), + ['HELLO', '3', 'AUTH', 'username', 'password'] + ); + }); - it('with protover, auth, clientName', () => { - assert.deepEqual( - transformArguments({ - protover: 3, - auth: { - username: 'username', - password: 'password' - }, - clientName: 'clientName' - }), - ['HELLO', '3', 'AUTH', 'username', 'password', 'SETNAME', 'clientName'] - ); - }); + it('with protover, SETNAME', () => { + assert.deepEqual( + HELLO.transformArguments(3, { + SETNAME: 'name' + }), + ['HELLO', '3', 'SETNAME', 'name'] + ); }); - testUtils.testWithClient('client.hello', async client => { - const reply = await client.hello(); - assert.equal(reply.server, 'redis'); - assert.equal(typeof reply.version, 'string'); - assert.equal(reply.proto, 2); - assert.equal(typeof reply.id, 'number'); - assert.equal(reply.mode, 'standalone'); - assert.equal(reply.role, 'master'); - assert.deepEqual(reply.modules, []); - }, { - ...GLOBAL.SERVERS.OPEN, - minimumDockerVersion: [6, 2] + it('with protover, AUTH, SETNAME', () => { + assert.deepEqual( + HELLO.transformArguments(3, { + AUTH: { + username: 'username', + password: 'password' + }, + SETNAME: 'name' + }), + ['HELLO', '3', 'AUTH', 'username', 'password', 'SETNAME', 'name'] + ); }); + }); + + testUtils.testWithClient('client.hello', async client => { + const reply = await client.hello(); + assert.equal(reply.server, 'redis'); + assert.equal(typeof reply.version, 'string'); + assert.equal(reply.proto, 2); + assert.equal(typeof reply.id, 'number'); + assert.equal(reply.mode, 'standalone'); + assert.equal(reply.role, 'master'); + assert.ok(reply.modules instanceof Array); + }, { + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [6, 2] + }); }); diff --git a/packages/client/lib/commands/HELLO.ts b/packages/client/lib/commands/HELLO.ts index d943f2e4c35..0fb2960d028 100644 --- a/packages/client/lib/commands/HELLO.ts +++ b/packages/client/lib/commands/HELLO.ts @@ -1,65 +1,59 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { AuthOptions } from './AUTH'; - -interface HelloOptions { - protover: number; - auth?: Required; - clientName?: string; +import { RedisArgument, RespVersions, TuplesToMapReply, BlobStringReply, NumberReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; + +export interface HelloOptions { + protover?: RespVersions; + AUTH?: { + username: RedisArgument; + password: RedisArgument; + }; + SETNAME?: string; } -export function transformArguments(options?: HelloOptions): RedisCommandArguments { - const args: RedisCommandArguments = ['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); - } +export type HelloReply = TuplesToMapReply<[ + [BlobStringReply<'server'>, BlobStringReply], + [BlobStringReply<'version'>, BlobStringReply], + [BlobStringReply<'proto'>, NumberReply], + [BlobStringReply<'id'>, NumberReply], + [BlobStringReply<'mode'>, BlobStringReply], + [BlobStringReply<'role'>, BlobStringReply], + [BlobStringReply<'modules'>, ArrayReply] +]>; + +export default { + transformArguments(protover?: RespVersions, options?: HelloOptions) { + const args: Array = ['HELLO']; + + if (protover) { + args.push(protover.toString()); + + if (options?.AUTH) { + args.push( + 'AUTH', + options.AUTH.username, + options.AUTH.password + ); + } + + if (options?.SETNAME) { + args.push( + 'SETNAME', + options.SETNAME + ); + } } - + return args; -} - -type HelloRawReply = [ - _: never, - server: RedisCommandArgument, - _: never, - version: RedisCommandArgument, - _: never, - proto: number, - _: never, - id: number, - _: never, - mode: RedisCommandArgument, - _: never, - role: RedisCommandArgument, - _: never, - modules: Array -]; - -interface HelloTransformedReply { - server: RedisCommandArgument; - version: RedisCommandArgument; - proto: number; - id: number; - mode: RedisCommandArgument; - role: RedisCommandArgument; - 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] - }; -} + }, + transformReply: { + 2: (reply: UnwrapReply>) => ({ + server: reply[1], + version: reply[3], + proto: reply[5], + id: reply[7], + mode: reply[9], + role: reply[11], + modules: reply[13] + }), + 3: undefined as unknown as () => HelloReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/HEXISTS.spec.ts b/packages/client/lib/commands/HEXISTS.spec.ts index 3764319c123..69ca6fa765f 100644 --- a/packages/client/lib/commands/HEXISTS.spec.ts +++ b/packages/client/lib/commands/HEXISTS.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HEXISTS'; +import HEXISTS from './HEXISTS'; describe('HEXISTS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HEXISTS', 'key', 'field'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HEXISTS.transformArguments('key', 'field'), + ['HEXISTS', 'key', 'field'] + ); + }); - testUtils.testWithClient('client.hExists', async client => { - assert.equal( - await client.hExists('key', 'field'), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hExists', async client => { + assert.equal( + await client.hExists('key', 'field'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HEXISTS.ts b/packages/client/lib/commands/HEXISTS.ts index 289be20aa83..dc7e937ee78 100644 --- a/packages/client/lib/commands/HEXISTS.ts +++ b/packages/client/lib/commands/HEXISTS.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, field: RedisArgument) { return ['HEXISTS', key, field]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply<0 | 1> +} as const satisfies Command; diff --git a/packages/client/lib/commands/HEXPIRE.spec.ts b/packages/client/lib/commands/HEXPIRE.spec.ts index 3714f617f58..71c48b7e884 100644 --- a/packages/client/lib/commands/HEXPIRE.spec.ts +++ b/packages/client/lib/commands/HEXPIRE.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HEXPIRE'; +import HEXPIRE from './HEXPIRE'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HEXPIRE', () => { @@ -9,21 +9,21 @@ describe('HEXPIRE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field', 1), + HEXPIRE.transformArguments('key', 'field', 1), ['HEXPIRE', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2'], 1), + HEXPIRE.transformArguments('key', ['field1', 'field2'], 1), ['HEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); it('with set option', () => { assert.deepEqual( - transformArguments('key', ['field1'], 1, 'NX'), + HEXPIRE.transformArguments('key', ['field1'], 1, 'NX'), ['HEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1'] ); }); diff --git a/packages/client/lib/commands/HEXPIRE.ts b/packages/client/lib/commands/HEXPIRE.ts index 938f9039939..34b52c1db68 100644 --- a/packages/client/lib/commands/HEXPIRE.ts +++ b/packages/client/lib/commands/HEXPIRE.ts @@ -1,44 +1,35 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { Command, RedisArgument } from '../RESP/types'; +import { pushVariadicArgument } from './generic-transformers'; -/** - * @readonly - * @enum {number} - */ export const HASH_EXPIRATION = { - /** @property {number} */ /** The field does not exist */ FIELD_NOT_EXISTS: -2, - /** @property {number} */ /** Specified NX | XX | GT | LT condition not met */ CONDITION_NOT_MET: 0, - /** @property {number} */ /** Expiration time was set or updated */ UPDATED: 1, - /** @property {number} */ /** Field deleted because the specified expiration time is in the past */ DELETED: 2 } as const; export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION]; -export const FIRST_KEY_INDEX = 1; +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, + fields: RedisArgument | Array, + seconds: number, + mode?: 'NX' | 'XX' | 'GT' | 'LT', + ) { + const args = ['HEXPIRE', key, seconds.toString()]; -export function transformArguments( - key: RedisCommandArgument, - fields: RedisCommandArgument| Array, - seconds: number, - mode?: 'NX' | 'XX' | 'GT' | 'LT', -) { - const args = ['HEXPIRE', key, seconds.toString()]; + if (mode) { + args.push(mode); + } - if (mode) { - args.push(mode); - } + args.push('FIELDS'); - args.push('FIELDS'); - - return pushVerdictArgument(args, fields); -} - -export declare function transformReply(): Array; \ No newline at end of file + return pushVariadicArgument(args, fields); + }, + transformReply: undefined as unknown as () => Array +} as const satisfies Command; diff --git a/packages/client/lib/commands/HEXPIREAT.spec.ts b/packages/client/lib/commands/HEXPIREAT.spec.ts index 1c65fb61773..1f87300214c 100644 --- a/packages/client/lib/commands/HEXPIREAT.spec.ts +++ b/packages/client/lib/commands/HEXPIREAT.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HEXPIREAT'; +import HEXPIREAT from './HEXPIREAT'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HEXPIREAT', () => { @@ -9,14 +9,14 @@ describe('HEXPIREAT', () => { describe('transformArguments', () => { it('string + number', () => { assert.deepEqual( - transformArguments('key', 'field', 1), + HEXPIREAT.transformArguments('key', 'field', 1), ['HEXPIREAT', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array + number', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2'], 1), + HEXPIREAT.transformArguments('key', ['field1', 'field2'], 1), ['HEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); @@ -25,14 +25,14 @@ describe('HEXPIREAT', () => { const d = new Date(); assert.deepEqual( - transformArguments('key', ['field1'], d), + HEXPIREAT.transformArguments('key', ['field1'], d), ['HEXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString(), 'FIELDS', '1', 'field1'] ); }); it('with set option', () => { assert.deepEqual( - transformArguments('key', 'field1', 1, 'GT'), + HEXPIREAT.transformArguments('key', 'field1', 1, 'GT'), ['HEXPIREAT', 'key', '1', 'GT', 'FIELDS', '1', 'field1'] ); }); diff --git a/packages/client/lib/commands/HEXPIREAT.ts b/packages/client/lib/commands/HEXPIREAT.ts index 58c52d3a1f6..5a49951f1cd 100644 --- a/packages/client/lib/commands/HEXPIREAT.ts +++ b/packages/client/lib/commands/HEXPIREAT.ts @@ -1,28 +1,28 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument, transformEXAT } from './generic-transformers'; +import { Command, RedisArgument } from '../RESP/types'; +import { pushVariadicArgument, RedisVariadicArgument, transformEXAT } from './generic-transformers'; import { HashExpiration } from './HEXPIRE'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - fields: RedisCommandArgument | Array, - timestamp: number | Date, - mode?: 'NX' | 'XX' | 'GT' | 'LT' -) { - const args = [ - 'HEXPIREAT', - key, - transformEXAT(timestamp) - ]; - - if (mode) { - args.push(mode); - } - - args.push('FIELDS') - - return pushVerdictArgument(args, fields); -} - -export declare function transformReply(): Array; \ No newline at end of file +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + fields: RedisVariadicArgument, + timestamp: number | Date, + mode?: 'NX' | 'XX' | 'GT' | 'LT' + ) { + const args = [ + 'HEXPIREAT', + key, + transformEXAT(timestamp) + ]; + + if (mode) { + args.push(mode); + } + + args.push('FIELDS') + + return pushVariadicArgument(args, fields); + }, + transformReply: undefined as unknown as () => Array +} as const satisfies Command; diff --git a/packages/client/lib/commands/HEXPIRETIME.spec.ts b/packages/client/lib/commands/HEXPIRETIME.spec.ts index 9c3eb024bed..2335ec91726 100644 --- a/packages/client/lib/commands/HEXPIRETIME.spec.ts +++ b/packages/client/lib/commands/HEXPIRETIME.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { HASH_EXPIRATION_TIME, transformArguments } from './HEXPIRETIME'; +import HEXPIRETIME, { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HEXPIRETIME', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -8,14 +8,14 @@ describe('HEXPIRETIME', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field'), + HEXPIRETIME.transformArguments('key', 'field'), ['HEXPIRETIME', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2']), + HEXPIRETIME.transformArguments('key', ['field1', 'field2']), ['HEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HEXPIRETIME.ts b/packages/client/lib/commands/HEXPIRETIME.ts index 01764b1032d..7edf1309002 100644 --- a/packages/client/lib/commands/HEXPIRETIME.ts +++ b/packages/client/lib/commands/HEXPIRETIME.ts @@ -1,21 +1,18 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { ArrayReply, Command, NumberReply, RedisArgument } from '../RESP/types'; +import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; export const HASH_EXPIRATION_TIME = { - /** @property {number} */ /** The field does not exist */ FIELD_NOT_EXISTS: -2, - /** @property {number} */ /** The field exists but has no associated expire */ NO_EXPIRATION: -1, } as const; -export const FIRST_KEY_INDEX = 1 - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { - return pushVerdictArgument(['HEXPIRETIME', key, 'FIELDS'], fields); -} - -export declare function transformReply(): Array; \ No newline at end of file +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { + return pushVariadicArgument(['HEXPIRETIME', key, 'FIELDS'], fields); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HGET.spec.ts b/packages/client/lib/commands/HGET.spec.ts index 6b6d0a3ee22..397f22b5604 100644 --- a/packages/client/lib/commands/HGET.spec.ts +++ b/packages/client/lib/commands/HGET.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HGET'; +import HGET from './HGET'; describe('HGET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HGET', 'key', 'field'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HGET.transformArguments('key', 'field'), + ['HGET', 'key', 'field'] + ); + }); - testUtils.testWithClient('client.hGet', async client => { - assert.equal( - await client.hGet('key', 'field'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hGet', async client => { + assert.equal( + await client.hGet('key', 'field'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HGET.ts b/packages/client/lib/commands/HGET.ts index fcfd31e6172..d83f84e24fa 100644 --- a/packages/client/lib/commands/HGET.ts +++ b/packages/client/lib/commands/HGET.ts @@ -1,14 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, field: RedisArgument) { return ['HGET', key, field]; -} - -export declare function transformReply(): RedisCommandArgument | undefined; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HGETALL.spec.ts b/packages/client/lib/commands/HGETALL.spec.ts index fcd1a30457c..93d122bae07 100644 --- a/packages/client/lib/commands/HGETALL.spec.ts +++ b/packages/client/lib/commands/HGETALL.spec.ts @@ -1,41 +1,34 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } 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 - } - }) - ); - }); - }); + testUtils.testAll('hGetAll empty', async client => { + assert.deepEqual( + await client.hGetAll('key'), + Object.create(null) + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); - testUtils.testWithClient('client.hGetAll', async client => { - assert.deepEqual( - await client.hGetAll('key'), - Object.create(null) - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hGetAll with value', async client => { + const [, reply] = await Promise.all([ + client.hSet('key', 'field', 'value'), + client.hGetAll('key') + ]); + assert.deepEqual( + reply, + Object.create(null, { + field: { + value: 'value', + enumerable: true + } + }) + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HGETALL.ts b/packages/client/lib/commands/HGETALL.ts index bf51760ff0e..f1f0ac50bcb 100644 --- a/packages/client/lib/commands/HGETALL.ts +++ b/packages/client/lib/commands/HGETALL.ts @@ -1,13 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, MapReply, BlobStringReply, Command } from '../RESP/types'; +import { transformTuplesReply } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export const TRANSFORM_LEGACY_REPLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['HGETALL', key]; -} - -export { transformTuplesReply as transformReply } from './generic-transformers'; + }, + TRANSFORM_LEGACY_REPLY: true, + transformReply: { + 2: transformTuplesReply, + 3: undefined as unknown as () => MapReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/HINCRBY.spec.ts b/packages/client/lib/commands/HINCRBY.spec.ts index de406217921..7718fe955eb 100644 --- a/packages/client/lib/commands/HINCRBY.spec.ts +++ b/packages/client/lib/commands/HINCRBY.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HINCRBY'; +import HINCRBY from './HINCRBY'; describe('HINCRBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field', 1), - ['HINCRBY', 'key', 'field', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HINCRBY.transformArguments('key', 'field', 1), + ['HINCRBY', 'key', 'field', '1'] + ); + }); - testUtils.testWithClient('client.hIncrBy', async client => { - assert.equal( - await client.hIncrBy('key', 'field', 1), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hIncrBy', async client => { + assert.equal( + await client.hIncrBy('key', 'field', 1), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HINCRBY.ts b/packages/client/lib/commands/HINCRBY.ts index b2cf6eefe89..cb7f62ebef5 100644 --- a/packages/client/lib/commands/HINCRBY.ts +++ b/packages/client/lib/commands/HINCRBY.ts @@ -1,13 +1,18 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + field: RedisArgument, increment: number -): RedisCommandArguments { - return ['HINCRBY', key, field, increment.toString()]; -} - -export declare function transformReply(): number; + ) { + return [ + 'HINCRBY', + key, + field, + increment.toString() + ]; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HINCRBYFLOAT.spec.ts b/packages/client/lib/commands/HINCRBYFLOAT.spec.ts index bd0147a3481..6c265dc6d10 100644 --- a/packages/client/lib/commands/HINCRBYFLOAT.spec.ts +++ b/packages/client/lib/commands/HINCRBYFLOAT.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HINCRBYFLOAT'; +import HINCRBYFLOAT from './HINCRBYFLOAT'; describe('HINCRBYFLOAT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field', 1.5), - ['HINCRBYFLOAT', 'key', 'field', '1.5'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HINCRBYFLOAT.transformArguments('key', 'field', 1.5), + ['HINCRBYFLOAT', 'key', 'field', '1.5'] + ); + }); - testUtils.testWithClient('client.hIncrByFloat', async client => { - assert.equal( - await client.hIncrByFloat('key', 'field', 1.5), - '1.5' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hIncrByFloat', async client => { + assert.equal( + await client.hIncrByFloat('key', 'field', 1.5), + '1.5' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HINCRBYFLOAT.ts b/packages/client/lib/commands/HINCRBYFLOAT.ts index 0e2de6e9b29..a4eea75c827 100644 --- a/packages/client/lib/commands/HINCRBYFLOAT.ts +++ b/packages/client/lib/commands/HINCRBYFLOAT.ts @@ -1,13 +1,18 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + field: RedisArgument, increment: number -): RedisCommandArguments { - return ['HINCRBYFLOAT', key, field, increment.toString()]; -} - -export declare function transformReply(): number; + ) { + return [ + 'HINCRBYFLOAT', + key, + field, + increment.toString() + ]; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HKEYS.spec.ts b/packages/client/lib/commands/HKEYS.spec.ts index f94538f67d3..dada7b4d6fd 100644 --- a/packages/client/lib/commands/HKEYS.spec.ts +++ b/packages/client/lib/commands/HKEYS.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HKEYS'; +import HKEYS from './HKEYS'; describe('HKEYS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['HKEYS', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HKEYS.transformArguments('key'), + ['HKEYS', 'key'] + ); + }); - testUtils.testWithClient('client.hKeys', async client => { - assert.deepEqual( - await client.hKeys('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hKeys', async client => { + assert.deepEqual( + await client.hKeys('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HKEYS.ts b/packages/client/lib/commands/HKEYS.ts index 3d629733d0e..00af43f7a40 100644 --- a/packages/client/lib/commands/HKEYS.ts +++ b/packages/client/lib/commands/HKEYS.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['HKEYS', key]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HLEN.spec.ts b/packages/client/lib/commands/HLEN.spec.ts index be9d4b13a7d..2457a261299 100644 --- a/packages/client/lib/commands/HLEN.spec.ts +++ b/packages/client/lib/commands/HLEN.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HLEN'; +import HLEN from './HLEN'; describe('HLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['HLEN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HLEN.transformArguments('key'), + ['HLEN', 'key'] + ); + }); - testUtils.testWithClient('client.hLen', async client => { - assert.equal( - await client.hLen('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hLen', async client => { + assert.equal( + await client.hLen('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HLEN.ts b/packages/client/lib/commands/HLEN.ts index 15a93d408d7..8f156d303e2 100644 --- a/packages/client/lib/commands/HLEN.ts +++ b/packages/client/lib/commands/HLEN.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['HLEN', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HMGET.spec.ts b/packages/client/lib/commands/HMGET.spec.ts index a7c934b760d..99d94a6d375 100644 --- a/packages/client/lib/commands/HMGET.spec.ts +++ b/packages/client/lib/commands/HMGET.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HMGET'; +import HMGET from './HMGET'; describe('HMGET', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HMGET', 'key', 'field'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + HMGET.transformArguments('key', 'field'), + ['HMGET', 'key', 'field'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['field1', 'field2']), - ['HMGET', 'key', 'field1', 'field2'] - ); - }); + it('array', () => { + assert.deepEqual( + HMGET.transformArguments('key', ['field1', 'field2']), + ['HMGET', 'key', 'field1', 'field2'] + ); }); + }); - testUtils.testWithClient('client.hmGet', async client => { - assert.deepEqual( - await client.hmGet('key', 'field'), - [null] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hmGet', async client => { + assert.deepEqual( + await client.hmGet('key', 'field'), + [null] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HMGET.ts b/packages/client/lib/commands/HMGET.ts index 64b4014abeb..df28a64be09 100644 --- a/packages/client/lib/commands/HMGET.ts +++ b/packages/client/lib/commands/HMGET.ts @@ -1,15 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - fields: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['HMGET', key], fields); -} - -export declare function transformReply(): Array; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + fields: RedisVariadicArgument + ) { + return pushVariadicArguments(['HMGET', key], fields); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HPERSIST.spec.ts b/packages/client/lib/commands/HPERSIST.spec.ts index 8cf3f1fe221..05e225e8ead 100644 --- a/packages/client/lib/commands/HPERSIST.spec.ts +++ b/packages/client/lib/commands/HPERSIST.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HPERSIST'; +import HPERSIST from './HPERSIST'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HPERSIST', () => { @@ -9,14 +9,14 @@ describe('HPERSIST', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field'), + HPERSIST.transformArguments('key', 'field'), ['HPERSIST', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2']), + HPERSIST.transformArguments('key', ['field1', 'field2']), ['HPERSIST', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HPERSIST.ts b/packages/client/lib/commands/HPERSIST.ts index 862a7548ac1..3843fd80a5e 100644 --- a/packages/client/lib/commands/HPERSIST.ts +++ b/packages/client/lib/commands/HPERSIST.ts @@ -1,10 +1,11 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; +import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { - return pushVerdictArgument(['HPERSIST', key, 'FIELDS'], fields); -} - -export declare function transformReply(): Array | null; \ No newline at end of file +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { + return pushVariadicArgument(['HPERSIST', key, 'FIELDS'], fields); + }, + transformReply: undefined as unknown as () => ArrayReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HPEXPIRE.spec.ts b/packages/client/lib/commands/HPEXPIRE.spec.ts index 852d9f5bd21..febcb0bc96b 100644 --- a/packages/client/lib/commands/HPEXPIRE.spec.ts +++ b/packages/client/lib/commands/HPEXPIRE.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HPEXPIRE'; +import HPEXPIRE from './HPEXPIRE'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HEXPIRE', () => { @@ -9,21 +9,21 @@ describe('HEXPIRE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field', 1), + HPEXPIRE.transformArguments('key', 'field', 1), ['HPEXPIRE', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2'], 1), + HPEXPIRE.transformArguments('key', ['field1', 'field2'], 1), ['HPEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); it('with set option', () => { assert.deepEqual( - transformArguments('key', ['field1'], 1, 'NX'), + HPEXPIRE.transformArguments('key', ['field1'], 1, 'NX'), ['HPEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1'] ); }); diff --git a/packages/client/lib/commands/HPEXPIRE.ts b/packages/client/lib/commands/HPEXPIRE.ts index afbb056ed4e..58624f9163a 100644 --- a/packages/client/lib/commands/HPEXPIRE.ts +++ b/packages/client/lib/commands/HPEXPIRE.ts @@ -1,24 +1,24 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { ArrayReply, Command, NullReply, RedisArgument } from '../RESP/types'; +import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; import { HashExpiration } from "./HEXPIRE"; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - fields: RedisCommandArgument | Array, - ms: number, - mode?: 'NX' | 'XX' | 'GT' | 'LT', -) { - const args = ['HPEXPIRE', key, ms.toString()]; - - if (mode) { - args.push(mode); - } - - args.push('FIELDS') - - return pushVerdictArgument(args, fields); -} - -export declare function transformReply(): Array | null; \ No newline at end of file +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + fields: RedisVariadicArgument, + ms: number, + mode?: 'NX' | 'XX' | 'GT' | 'LT', + ) { + const args = ['HPEXPIRE', key, ms.toString()]; + + if (mode) { + args.push(mode); + } + + args.push('FIELDS') + + return pushVariadicArgument(args, fields); + }, + transformReply: undefined as unknown as () => ArrayReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HPEXPIREAT.spec.ts b/packages/client/lib/commands/HPEXPIREAT.spec.ts index 9747cca1a2d..f91bf967cf8 100644 --- a/packages/client/lib/commands/HPEXPIREAT.spec.ts +++ b/packages/client/lib/commands/HPEXPIREAT.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HPEXPIREAT'; +import HPEXPIREAT from './HPEXPIREAT'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HPEXPIREAT', () => { @@ -9,14 +9,14 @@ describe('HPEXPIREAT', () => { describe('transformArguments', () => { it('string + number', () => { assert.deepEqual( - transformArguments('key', 'field', 1), + HPEXPIREAT.transformArguments('key', 'field', 1), ['HPEXPIREAT', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array + number', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2'], 1), + HPEXPIREAT.transformArguments('key', ['field1', 'field2'], 1), ['HPEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); @@ -24,14 +24,14 @@ describe('HPEXPIREAT', () => { it('date', () => { const d = new Date(); assert.deepEqual( - transformArguments('key', ['field1'], d), + HPEXPIREAT.transformArguments('key', ['field1'], d), ['HPEXPIREAT', 'key', d.getTime().toString(), 'FIELDS', '1', 'field1'] ); }); it('with set option', () => { assert.deepEqual( - transformArguments('key', ['field1'], 1, 'XX'), + HPEXPIREAT.transformArguments('key', ['field1'], 1, 'XX'), ['HPEXPIREAT', 'key', '1', 'XX', 'FIELDS', '1', 'field1'] ); }); diff --git a/packages/client/lib/commands/HPEXPIREAT.ts b/packages/client/lib/commands/HPEXPIREAT.ts index b6e01d8ee5c..a6250d99432 100644 --- a/packages/client/lib/commands/HPEXPIREAT.ts +++ b/packages/client/lib/commands/HPEXPIREAT.ts @@ -1,25 +1,24 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument, transformEXAT, transformPXAT } from './generic-transformers'; +import { ArrayReply, Command, NullReply, RedisArgument } from '../RESP/types'; +import { pushVariadicArgument, RedisVariadicArgument, transformPXAT } from './generic-transformers'; import { HashExpiration } from './HEXPIRE'; -export const FIRST_KEY_INDEX = 1; -export const IS_READ_ONLY = true; +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + fields: RedisVariadicArgument, + timestamp: number | Date, + mode?: 'NX' | 'XX' | 'GT' | 'LT' + ) { + const args = ['HPEXPIREAT', key, transformPXAT(timestamp)]; -export function transformArguments( - key: RedisCommandArgument, - fields: RedisCommandArgument | Array, - timestamp: number | Date, - mode?: 'NX' | 'XX' | 'GT' | 'LT' -) { - const args = ['HPEXPIREAT', key, transformPXAT(timestamp)]; + if (mode) { + args.push(mode); + } - if (mode) { - args.push(mode); - } + args.push('FIELDS') - args.push('FIELDS') - - return pushVerdictArgument(args, fields); -} - -export declare function transformReply(): Array | null; \ No newline at end of file + return pushVariadicArgument(args, fields); + }, + transformReply: undefined as unknown as () => ArrayReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HPEXPIRETIME.spec.ts b/packages/client/lib/commands/HPEXPIRETIME.spec.ts index ff03b73c71d..a66988c428c 100644 --- a/packages/client/lib/commands/HPEXPIRETIME.spec.ts +++ b/packages/client/lib/commands/HPEXPIRETIME.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HPEXPIRETIME'; +import HPEXPIRETIME from './HPEXPIRETIME'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HPEXPIRETIME', () => { @@ -9,14 +9,14 @@ describe('HPEXPIRETIME', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field'), + HPEXPIRETIME.transformArguments('key', 'field'), ['HPEXPIRETIME', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2']), + HPEXPIRETIME.transformArguments('key', ['field1', 'field2']), ['HPEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts index 22a794ccefa..acdccf25119 100644 --- a/packages/client/lib/commands/HPEXPIRETIME.ts +++ b/packages/client/lib/commands/HPEXPIRETIME.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; +import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { - return pushVerdictArgument(['HPEXPIRETIME', key, 'FIELDS'], fields); -} - -export declare function transformReply(): Array | null; \ No newline at end of file +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { + return pushVariadicArgument(['HPEXPIRETIME', key, 'FIELDS'], fields); + }, + transformReply: undefined as unknown as () => ArrayReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HPTTL.spec.ts b/packages/client/lib/commands/HPTTL.spec.ts index ddca26ea85b..7280ef841ca 100644 --- a/packages/client/lib/commands/HPTTL.spec.ts +++ b/packages/client/lib/commands/HPTTL.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HPTTL'; +import HPTTL from './HPTTL'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HPTTL', () => { @@ -9,14 +9,14 @@ describe('HPTTL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field'), + HPTTL.transformArguments('key', 'field'), ['HPTTL', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2']), + HPTTL.transformArguments('key', ['field1', 'field2']), ['HPTTL', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HPTTL.ts b/packages/client/lib/commands/HPTTL.ts index 988b805c0c9..4ab069db74e 100644 --- a/packages/client/lib/commands/HPTTL.ts +++ b/packages/client/lib/commands/HPTTL.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; +import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { - return pushVerdictArgument(['HPTTL', key, 'FIELDS'], fields); -} - -export declare function transformReply(): Array | null; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { + return pushVariadicArgument(['HPTTL', key, 'FIELDS'], fields); + }, + transformReply: undefined as unknown as () => ArrayReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HRANDFIELD.spec.ts b/packages/client/lib/commands/HRANDFIELD.spec.ts index df0a4fc7a1d..33f2d281803 100644 --- a/packages/client/lib/commands/HRANDFIELD.spec.ts +++ b/packages/client/lib/commands/HRANDFIELD.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HRANDFIELD'; +import HRANDFIELD from './HRANDFIELD'; describe('HRANDFIELD', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['HRANDFIELD', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HRANDFIELD.transformArguments('key'), + ['HRANDFIELD', 'key'] + ); + }); - testUtils.testWithClient('client.hRandField', async client => { - assert.equal( - await client.hRandField('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hRandField', async client => { + assert.equal( + await client.hRandField('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HRANDFIELD.ts b/packages/client/lib/commands/HRANDFIELD.ts index a2c70aabd52..be878e244a0 100644 --- a/packages/client/lib/commands/HRANDFIELD.ts +++ b/packages/client/lib/commands/HRANDFIELD.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['HRANDFIELD', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts b/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts index 4bfce0f7e23..99788dc4962 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HRANDFIELD_COUNT'; +import HRANDFIELD_COUNT from './HRANDFIELD_COUNT'; describe('HRANDFIELD COUNT', () => { - testUtils.isVersionGreaterThanHook([6, 2, 5]); + testUtils.isVersionGreaterThanHook([6, 2, 5]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['HRANDFIELD', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HRANDFIELD_COUNT.transformArguments('key', 1), + ['HRANDFIELD', 'key', '1'] + ); + }); - testUtils.testWithClient('client.hRandFieldCount', async client => { - assert.deepEqual( - await client.hRandFieldCount('key', 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hRandFieldCount', async client => { + assert.deepEqual( + await client.hRandFieldCount('key', 1), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT.ts b/packages/client/lib/commands/HRANDFIELD_COUNT.ts index 01b8df63273..4b6f42a115b 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT.ts @@ -1,16 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformHRandFieldArguments } from './HRANDFIELD'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './HRANDFIELD'; - -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { - return [ - ...transformHRandFieldArguments(key), - count.toString() - ]; -} - -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, count: number) { + return ['HRANDFIELD', key, count.toString()]; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts index c4e6409a726..e69de29bb2d 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.spec.ts @@ -1,21 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HRANDFIELD_COUNT_WITHVALUES'; - -describe('HRANDFIELD COUNT WITHVALUES', () => { - testUtils.isVersionGreaterThanHook([6, 2, 5]); - - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['HRANDFIELD', 'key', '1', 'WITHVALUES'] - ); - }); - - testUtils.testWithClient('client.hRandFieldCountWithValues', async client => { - assert.deepEqual( - await client.hRandFieldCountWithValues('key', 1), - Object.create(null) - ); - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts index 3e09dbb9a14..ab36183c4ad 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts @@ -1,16 +1,39 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformHRandFieldCountArguments } from './HRANDFIELD_COUNT'; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, UnwrapReply, Command } from '../RESP/types'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './HRANDFIELD_COUNT'; +export type HRandFieldCountWithValuesReply = Array<{ + field: BlobStringReply; + value: BlobStringReply; +}>; -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { - return [ - ...transformHRandFieldCountArguments(key, count), - 'WITHVALUES' - ]; -} +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, count: number) { + return ['HRANDFIELD', key, count.toString(), 'WITHVALUES']; + }, + transformReply: { + 2: (rawReply: UnwrapReply>) => { + const reply: HRandFieldCountWithValuesReply = []; -export { transformTuplesReply as transformReply } from './generic-transformers'; + let i = 0; + while (i < rawReply.length) { + reply.push({ + field: rawReply[i++], + value: rawReply[i++] + }); + } + + return reply; + }, + 3: (reply: UnwrapReply>>) => { + return reply.map(entry => { + const [field, value] = entry as unknown as UnwrapReply; + return { + field, + value + }; + }) satisfies HRandFieldCountWithValuesReply; + } + } +} as const satisfies Command; + \ No newline at end of file diff --git a/packages/client/lib/commands/HSCAN.spec.ts b/packages/client/lib/commands/HSCAN.spec.ts index 6757888a875..a5f3cdca16c 100644 --- a/packages/client/lib/commands/HSCAN.spec.ts +++ b/packages/client/lib/commands/HSCAN.spec.ts @@ -1,90 +1,82 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './HSCAN'; +import HSCAN 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'] - ); - }); + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + HSCAN.transformArguments('key', '0'), + ['HSCAN', 'key', '0'] + ); + }); - it('with MATCH & COUNT', () => { - assert.deepEqual( - transformArguments('key', 0, { - MATCH: 'pattern', - COUNT: 1 - }), - ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] - ); - }); + it('with MATCH', () => { + assert.deepEqual( + HSCAN.transformArguments('key', '0', { + MATCH: 'pattern' + }), + ['HSCAN', 'key', '0', 'MATCH', 'pattern'] + ); }); - describe('transformReply', () => { - it('without tuples', () => { - assert.deepEqual( - transformReply(['0', []]), - { - cursor: 0, - tuples: [] - } - ); - }); + it('with COUNT', () => { + assert.deepEqual( + HSCAN.transformArguments('key', '0', { + COUNT: 1 + }), + ['HSCAN', 'key', '0', 'COUNT', '1'] + ); + }); - it('with tuples', () => { - assert.deepEqual( - transformReply(['0', ['field', 'value']]), - { - cursor: 0, - tuples: [{ - field: 'field', - value: 'value' - }] - } - ); - }); + it('with MATCH & COUNT', () => { + assert.deepEqual( + HSCAN.transformArguments('key', '0', { + MATCH: 'pattern', + COUNT: 1 + }), + ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] + ); }); + }); - testUtils.testWithClient('client.hScan', async client => { - assert.deepEqual( - await client.hScan('key', 0), - { - cursor: 0, - tuples: [] - } - ); + describe('transformReply', () => { + it('without tuples', () => { + assert.deepEqual( + HSCAN.transformReply(['0' as any, []]), + { + cursor: '0', + entries: [] + } + ); + }); + + it('with tuples', () => { + assert.deepEqual( + HSCAN.transformReply(['0' as any, ['field', 'value'] as any]), + { + cursor: '0', + entries: [{ + field: 'field', + value: 'value' + }] + } + ); + }); + }); - await Promise.all([ - client.hSet('key', 'a', '1'), - client.hSet('key', 'b', '2') - ]); + testUtils.testWithClient('client.hScan', async client => { + const [, reply] = await Promise.all([ + client.hSet('key', 'field', 'value'), + client.hScan('key', '0') + ]); - assert.deepEqual( - await client.hScan('key', 0), - { - cursor: 0, - tuples: [{field: 'a', value: '1'}, {field: 'b', value: '2'}] - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, { + cursor: '0', + entries: [{ + field: 'field', + value: 'value' + }] + }); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/HSCAN.ts b/packages/client/lib/commands/HSCAN.ts index 5167693b604..db52db99fef 100644 --- a/packages/client/lib/commands/HSCAN.ts +++ b/packages/client/lib/commands/HSCAN.ts @@ -1,44 +1,34 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions, pushScanArguments } from './generic-transformers'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; +import { ScanCommonOptions, pushScanArguments } from './SCAN'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - cursor: number, - options?: ScanOptions -): RedisCommandArguments { - return pushScanArguments([ - 'HSCAN', - key - ], cursor, options); -} - -export type HScanRawReply = [RedisCommandArgument, Array]; - -export interface HScanTuple { - field: RedisCommandArgument; - value: RedisCommandArgument; -} - -interface HScanReply { - cursor: number; - tuples: Array; +export interface HScanEntry { + field: BlobStringReply; + value: BlobStringReply; } -export function transformReply([cursor, rawTuples]: HScanRawReply): HScanReply { - const parsedTuples = []; - for (let i = 0; i < rawTuples.length; i += 2) { - parsedTuples.push({ - field: rawTuples[i], - value: rawTuples[i + 1] - }); +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + cursor: RedisArgument, + options?: ScanCommonOptions + ) { + return pushScanArguments(['HSCAN', key], cursor, options); + }, + transformReply([cursor, rawEntries]: [BlobStringReply, Array]) { + const entries = []; + let i = 0; + while (i < rawEntries.length) { + entries.push({ + field: rawEntries[i++], + value: rawEntries[i++] + } satisfies HScanEntry); } return { - cursor: Number(cursor), - tuples: parsedTuples + cursor, + entries }; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts b/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts index 7e05b841e43..1283a116dc5 100644 --- a/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts +++ b/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts @@ -1,79 +1,82 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './HSCAN_NOVALUES'; +import HSCAN_NOVALUES from './HSCAN_NOVALUES'; +import { BlobStringReply } from '../RESP/types'; describe('HSCAN_NOVALUES', () => { - testUtils.isVersionGreaterThanHook([7, 4]); - - describe('transformArguments', () => { - it('cusror only', () => { - assert.deepEqual( - transformArguments('key', 0), - ['HSCAN', 'key', '0', 'NOVALUES'] - ); - }); + testUtils.isVersionGreaterThanHook([7,4]); + + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + HSCAN_NOVALUES.transformArguments('key', '0'), + ['HSCAN', 'key', '0', 'NOVALUES'] + ); + }); + + it('with MATCH', () => { + assert.deepEqual( + HSCAN_NOVALUES.transformArguments('key', '0', { + MATCH: 'pattern' + }), + ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'NOVALUES'] + ); + }); - it('with MATCH', () => { - assert.deepEqual( - transformArguments('key', 0, { - MATCH: 'pattern' - }), - ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'NOVALUES'] - ); - }); + it('with COUNT', () => { + assert.deepEqual( + HSCAN_NOVALUES.transformArguments('key', '0', { + COUNT: 1 + }), + ['HSCAN', 'key', '0', 'COUNT', '1', 'NOVALUES'] + ); + }); - it('with COUNT', () => { - assert.deepEqual( - transformArguments('key', 0, { - COUNT: 1 - }), - ['HSCAN', 'key', '0', 'COUNT', '1', 'NOVALUES'] - ); - }); + it('with MATCH & COUNT', () => { + assert.deepEqual( + HSCAN_NOVALUES.transformArguments('key', '0', { + MATCH: 'pattern', + COUNT: 1 + }), + ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1', 'NOVALUES'] + ); }); + }); - describe('transformReply', () => { - it('without keys', () => { - assert.deepEqual( - transformReply(['0', []]), - { - cursor: 0, - keys: [] - } - ); - }); + describe('transformReply', () => { + it('without keys', () => { + assert.deepEqual( + HSCAN_NOVALUES.transformReply(['0' as any, []]), + { + cursor: '0', + fields: [] + } + ); + }); - it('with keys', () => { - assert.deepEqual( - transformReply(['0', ['key1', 'key2']]), - { - cursor: 0, - keys: ['key1', 'key2'] - } - ); - }); + it('with keys', () => { + assert.deepEqual( + HSCAN_NOVALUES.transformReply(['0' as any, ['key1', 'key2'] as any]), + { + cursor: '0', + fields: ['key1', 'key2'] + } + ); }); + }); - testUtils.testWithClient('client.hScanNoValues', async client => { - assert.deepEqual( - await client.hScanNoValues('key', 0), - { - cursor: 0, - keys: [] - } - ); - await Promise.all([ - client.hSet('key', 'a', '1'), - client.hSet('key', 'b', '2') - ]); + testUtils.testWithClient('client.hScanNoValues', async client => { + const [, reply] = await Promise.all([ + client.hSet('key', 'field', 'value'), + client.hScanNoValues('key', '0') + ]); - assert.deepEqual( - await client.hScanNoValues('key', 0), - { - cursor: 0, - keys: ['a', 'b'] - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, { + cursor: '0', + fields: [ + 'field', + ] + }); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.ts b/packages/client/lib/commands/HSCAN_NOVALUES.ts index 37a929754c6..35ff861338c 100644 --- a/packages/client/lib/commands/HSCAN_NOVALUES.ts +++ b/packages/client/lib/commands/HSCAN_NOVALUES.ts @@ -1,27 +1,22 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions } from './generic-transformers'; -import { HScanRawReply, transformArguments as transformHScanArguments } from './HSCAN'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; +import { ScanCommonOptions, pushScanArguments } from './SCAN'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './HSCAN'; - -export function transformArguments( - key: RedisCommandArgument, - cursor: number, - options?: ScanOptions -): RedisCommandArguments { - const args = transformHScanArguments(key, cursor, options); +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + cursor: RedisArgument, + options?: ScanCommonOptions + ) { + const args = pushScanArguments(['HSCAN', key], cursor, options); args.push('NOVALUES'); return args; -} - -interface HScanNoValuesReply { - cursor: number; - keys: Array; -} - -export function transformReply([cursor, rawData]: HScanRawReply): HScanNoValuesReply { + }, + transformReply([cursor, fields]: [BlobStringReply, Array]) { return { - cursor: Number(cursor), - keys: rawData + cursor, + fields }; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/HSET.spec.ts b/packages/client/lib/commands/HSET.spec.ts index 73bc966f87a..eb5fdcd9b32 100644 --- a/packages/client/lib/commands/HSET.spec.ts +++ b/packages/client/lib/commands/HSET.spec.ts @@ -1,74 +1,70 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './HSET'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import HSET from './HSET'; describe('HSET', () => { - describe('transformArguments', () => { - describe('field, value', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'field', 'value'), - ['HSET', 'key', 'field', 'value'] - ); - }); - - it('number', () => { - assert.deepEqual( - transformArguments('key', 1, 2), - ['HSET', 'key', '1', '2'] - ); - }); + describe('transformArguments', () => { + describe('field, value', () => { + it('string', () => { + assert.deepEqual( + HSET.transformArguments('key', 'field', 'value'), + ['HSET', 'key', 'field', 'value'] + ); + }); - it('Buffer', () => { - assert.deepEqual( - transformArguments(Buffer.from('key'), Buffer.from('field'), Buffer.from('value')), - ['HSET', Buffer.from('key'), Buffer.from('field'), Buffer.from('value')] - ); - }); - }); + it('number', () => { + assert.deepEqual( + HSET.transformArguments('key', 1, 2), + ['HSET', 'key', '1', '2'] + ); + }); - it('Map', () => { - assert.deepEqual( - transformArguments('key', new Map([['field', 'value']])), - ['HSET', 'key', 'field', 'value'] - ); - }); + it('Buffer', () => { + assert.deepEqual( + HSET.transformArguments(Buffer.from('key'), Buffer.from('field'), Buffer.from('value')), + ['HSET', Buffer.from('key'), Buffer.from('field'), Buffer.from('value')] + ); + }); + }); - it('Array', () => { - assert.deepEqual( - transformArguments('key', [['field', 'value']]), - ['HSET', 'key', 'field', 'value'] - ); - }); + it('Map', () => { + assert.deepEqual( + HSET.transformArguments('key', new Map([['field', 'value']])), + ['HSET', 'key', 'field', 'value'] + ); + }); - describe('Object', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', { field: 'value' }), - ['HSET', 'key', 'field', 'value'] - ); - }); - - it('Buffer', () => { - assert.deepEqual( - transformArguments('key', { field: Buffer.from('value') }), - ['HSET', 'key', 'field', Buffer.from('value')] - ); - }); - }); + it('Array', () => { + assert.deepEqual( + HSET.transformArguments('key', [['field', 'value']]), + ['HSET', 'key', 'field', 'value'] + ); }); - testUtils.testWithClient('client.hSet', async client => { - assert.equal( - await client.hSet('key', 'field', 'value'), - 1 + describe('Object', () => { + it('string', () => { + assert.deepEqual( + HSET.transformArguments('key', { field: 'value' }), + ['HSET', 'key', 'field', 'value'] ); - }, GLOBAL.SERVERS.OPEN); + }); - testUtils.testWithCluster('cluster.hSet', async cluster => { - assert.equal( - await cluster.hSet('key', { field: 'value' }), - 1 + it('Buffer', () => { + assert.deepEqual( + HSET.transformArguments('key', { field: Buffer.from('value') }), + ['HSET', 'key', 'field', Buffer.from('value')] ); - }, GLOBAL.CLUSTERS.OPEN); + }); + }); + }); + + testUtils.testAll('hSet', async client => { + assert.equal( + await client.hSet('key', 'field', 'value'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HSET.ts b/packages/client/lib/commands/HSET.ts index 261ef98c779..e14aa9d06f6 100644 --- a/packages/client/lib/commands/HSET.ts +++ b/packages/client/lib/commands/HSET.ts @@ -1,73 +1,75 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; +export type HashTypes = RedisArgument | number; -type Types = RedisCommandArgument | number; +type HSETObject = Record; -type HSETObject = Record; +type HSETMap = Map; -type HSETMap = Map; +type HSETTuples = Array<[HashTypes, HashTypes]> | Array; -type HSETTuples = Array<[Types, Types]> | Array; +type GenericArguments = [key: RedisArgument]; -type GenericArguments = [key: RedisCommandArgument]; - -type SingleFieldArguments = [...generic: GenericArguments, field: Types, value: Types]; +type SingleFieldArguments = [...generic: GenericArguments, field: HashTypes, value: HashTypes]; type MultipleFieldsArguments = [...generic: GenericArguments, value: HSETObject | HSETMap | HSETTuples]; -export function transformArguments(...[ key, value, fieldValue ]: SingleFieldArguments | MultipleFieldsArguments): RedisCommandArguments { - const args: RedisCommandArguments = ['HSET', key]; +export type HSETArguments = SingleFieldArguments | MultipleFieldsArguments; + +export default { + FIRST_KEY_INDEX: 1, + transformArguments(...[key, value, fieldValue]: SingleFieldArguments | MultipleFieldsArguments) { + const args: Array = ['HSET', key]; - if (typeof value === 'string' || typeof value === 'number' || Buffer.isBuffer(value)) { - args.push( - convertValue(value), - convertValue(fieldValue!) - ); + if (typeof value === 'string' || typeof value === 'number' || value instanceof Buffer) { + args.push( + convertValue(value), + convertValue(fieldValue!) + ); } else if (value instanceof Map) { - pushMap(args, value); + pushMap(args, value); } else if (Array.isArray(value)) { - pushTuples(args, value); + pushTuples(args, value); } else { - pushObject(args, value); + pushObject(args, value); } return args; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; + +function pushMap(args: Array, map: HSETMap): void { + for (const [key, value] of map.entries()) { + args.push( + convertValue(key), + convertValue(value) + ); + } } -function pushMap(args: RedisCommandArguments, map: HSETMap): void { - for (const [key, value] of map.entries()) { - args.push( - convertValue(key), - convertValue(value) - ); +function pushTuples(args: Array, tuples: HSETTuples): void { + for (const tuple of tuples) { + if (Array.isArray(tuple)) { + pushTuples(args, tuple); + continue; } -} - -function pushTuples(args: RedisCommandArguments, tuples: HSETTuples): void { - for (const tuple of tuples) { - if (Array.isArray(tuple)) { - pushTuples(args, tuple); - continue; - } - args.push(convertValue(tuple)); - } + args.push(convertValue(tuple)); + } } -function pushObject(args: RedisCommandArguments, object: HSETObject): void { - for (const key of Object.keys(object)) { - args.push( - convertValue(key), - convertValue(object[key]) - ); - } +function pushObject(args: Array, object: HSETObject): void { + for (const key of Object.keys(object)) { + args.push( + convertValue(key), + convertValue(object[key]) + ); + } } -function convertValue(value: Types): RedisCommandArgument { - return typeof value === 'number' ? - value.toString() : - value; +function convertValue(value: HashTypes): RedisArgument { + return typeof value === 'number' ? + value.toString() : + value; } - -export declare function transformReply(): number; diff --git a/packages/client/lib/commands/HSETNX.spec.ts b/packages/client/lib/commands/HSETNX.spec.ts index 190fa50ae97..522732624e1 100644 --- a/packages/client/lib/commands/HSETNX.spec.ts +++ b/packages/client/lib/commands/HSETNX.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HSETNX'; +import HSETNX from './HSETNX'; describe('HSETNX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field', 'value'), - ['HSETNX', 'key', 'field', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HSETNX.transformArguments('key', 'field', 'value'), + ['HSETNX', 'key', 'field', 'value'] + ); + }); - testUtils.testWithClient('client.hSetNX', async client => { - assert.equal( - await client.hSetNX('key', 'field', 'value'), - true - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hSetNX', async client => { + assert.equal( + await client.hSetNX('key', 'field', 'value'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HSETNX.ts b/packages/client/lib/commands/HSETNX.ts index 9ac6ef0edd8..d26c42a76b4 100644 --- a/packages/client/lib/commands/HSETNX.ts +++ b/packages/client/lib/commands/HSETNX.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, Command, NumberReply } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument, - value: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + field: RedisArgument, + value: RedisArgument + ) { return ['HSETNX', key, field, value]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply<0 | 1> +} as const satisfies Command; diff --git a/packages/client/lib/commands/HSTRLEN.spec.ts b/packages/client/lib/commands/HSTRLEN.spec.ts index 79c3150211e..59b737b692b 100644 --- a/packages/client/lib/commands/HSTRLEN.spec.ts +++ b/packages/client/lib/commands/HSTRLEN.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HSTRLEN'; +import HSTRLEN from './HSTRLEN'; describe('HSTRLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'field'), - ['HSTRLEN', 'key', 'field'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HSTRLEN.transformArguments('key', 'field'), + ['HSTRLEN', 'key', 'field'] + ); + }); - testUtils.testWithClient('client.hStrLen', async client => { - assert.equal( - await client.hStrLen('key', 'field'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hStrLen', async client => { + assert.equal( + await client.hStrLen('key', 'field'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HSTRLEN.ts b/packages/client/lib/commands/HSTRLEN.ts index a820e6c5643..843c4baf7ef 100644 --- a/packages/client/lib/commands/HSTRLEN.ts +++ b/packages/client/lib/commands/HSTRLEN.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - field: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, field: RedisArgument) { return ['HSTRLEN', key, field]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HTTL.spec.ts b/packages/client/lib/commands/HTTL.spec.ts index 21b8b329a5d..df74c8a728e 100644 --- a/packages/client/lib/commands/HTTL.spec.ts +++ b/packages/client/lib/commands/HTTL.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HTTL'; +import HTTL from './HTTL'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HTTL', () => { @@ -9,14 +9,14 @@ describe('HTTL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - transformArguments('key', 'field'), + HTTL.transformArguments('key', 'field'), ['HTTL', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - transformArguments('key', ['field1', 'field2']), + HTTL.transformArguments('key', ['field1', 'field2']), ['HTTL', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HTTL.ts b/packages/client/lib/commands/HTTL.ts index d3eedd0db0e..66b50ff3e68 100644 --- a/packages/client/lib/commands/HTTL.ts +++ b/packages/client/lib/commands/HTTL.ts @@ -1,11 +1,11 @@ -import { RedisCommandArgument } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; +import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument, fields: RedisCommandArgument | Array) { - return pushVerdictArgument(['HTTL', key, 'FIELDS'], fields); -} - -export declare function transformReply(): Array | null; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { + return pushVariadicArgument(['HTTL', key, 'FIELDS'], fields); + }, + transformReply: undefined as unknown as () => ArrayReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HVALS.spec.ts b/packages/client/lib/commands/HVALS.spec.ts index d0a6c39ce5f..922aa588137 100644 --- a/packages/client/lib/commands/HVALS.spec.ts +++ b/packages/client/lib/commands/HVALS.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './HVALS'; +import HVALS from './HVALS'; describe('HVALS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['HVALS', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + HVALS.transformArguments('key'), + ['HVALS', 'key'] + ); + }); - testUtils.testWithClient('client.hVals', async client => { - assert.deepEqual( - await client.hVals('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('hVals', async client => { + assert.deepEqual( + await client.hVals('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/HVALS.ts b/packages/client/lib/commands/HVALS.ts index ef63fdc7f8a..ee4193f5cec 100644 --- a/packages/client/lib/commands/HVALS.ts +++ b/packages/client/lib/commands/HVALS.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['HVALS', key]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/INCR.spec.ts b/packages/client/lib/commands/INCR.spec.ts index 321d83edc54..67129760245 100644 --- a/packages/client/lib/commands/INCR.spec.ts +++ b/packages/client/lib/commands/INCR.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './INCR'; +import INCR from './INCR'; describe('INCR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['INCR', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + INCR.transformArguments('key'), + ['INCR', 'key'] + ); + }); - testUtils.testWithClient('client.incr', async client => { - assert.equal( - await client.incr('key'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('incr', async client => { + assert.equal( + await client.incr('key'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/INCR.ts b/packages/client/lib/commands/INCR.ts index 2f9a9adfe2b..eb38256c9dc 100644 --- a/packages/client/lib/commands/INCR.ts +++ b/packages/client/lib/commands/INCR.ts @@ -1,9 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument) { return ['INCR', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/INCRBY.spec.ts b/packages/client/lib/commands/INCRBY.spec.ts index a671d0ec259..d66c01acce5 100644 --- a/packages/client/lib/commands/INCRBY.spec.ts +++ b/packages/client/lib/commands/INCRBY.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './INCRBY'; +import INCRBY from './INCRBY'; -describe('INCR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['INCRBY', 'key', '1'] - ); - }); +describe('INCRBY', () => { + it('transformArguments', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1), + ['INCRBY', 'key', '1'] + ); + }); - testUtils.testWithClient('client.incrBy', async client => { - assert.equal( - await client.incrBy('key', 1), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('incrBy', async client => { + assert.equal( + await client.incrBy('key', 1), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/INCRBY.ts b/packages/client/lib/commands/INCRBY.ts index 75c61156d6b..5e94348fe63 100644 --- a/packages/client/lib/commands/INCRBY.ts +++ b/packages/client/lib/commands/INCRBY.ts @@ -1,12 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - increment: number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, increment: number) { return ['INCRBY', key, increment.toString()]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/INCRBYFLOAT.spec.ts b/packages/client/lib/commands/INCRBYFLOAT.spec.ts index b2dd5aa5da9..8bdd9c332de 100644 --- a/packages/client/lib/commands/INCRBYFLOAT.spec.ts +++ b/packages/client/lib/commands/INCRBYFLOAT.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './INCRBYFLOAT'; +import INCRBYFLOAT from './INCRBYFLOAT'; describe('INCRBYFLOAT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1.5), - ['INCRBYFLOAT', 'key', '1.5'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + INCRBYFLOAT.transformArguments('key', 1.5), + ['INCRBYFLOAT', 'key', '1.5'] + ); + }); - testUtils.testWithClient('client.incrByFloat', async client => { - assert.equal( - await client.incrByFloat('key', 1.5), - '1.5' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('incrByFloat', async client => { + assert.equal( + await client.incrByFloat('key', 1.5), + '1.5' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/INCRBYFLOAT.ts b/packages/client/lib/commands/INCRBYFLOAT.ts index ace3702339e..16f59e68c17 100644 --- a/packages/client/lib/commands/INCRBYFLOAT.ts +++ b/packages/client/lib/commands/INCRBYFLOAT.ts @@ -1,12 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - increment: number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, increment: number) { return ['INCRBYFLOAT', key, increment.toString()]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/INFO.spec.ts b/packages/client/lib/commands/INFO.spec.ts index 118682c7da1..c4555778729 100644 --- a/packages/client/lib/commands/INFO.spec.ts +++ b/packages/client/lib/commands/INFO.spec.ts @@ -1,20 +1,28 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './INFO'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import INFO from './INFO'; describe('INFO', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['INFO'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + INFO.transformArguments(), + ['INFO'] + ); + }); - it('server section', () => { - assert.deepEqual( - transformArguments('server'), - ['INFO', 'server'] - ); - }); + it('server section', () => { + assert.deepEqual( + INFO.transformArguments('server'), + ['INFO', 'server'] + ); }); + }); + + testUtils.testWithClient('client.info', async client => { + assert.equal( + typeof await client.info(), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/INFO.ts b/packages/client/lib/commands/INFO.ts index 8ab24221b26..9877c0cf66b 100644 --- a/packages/client/lib/commands/INFO.ts +++ b/packages/client/lib/commands/INFO.ts @@ -1,13 +1,16 @@ -export const IS_READ_ONLY = true; +import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types'; -export function transformArguments(section?: string): Array { - const args = ['INFO']; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(section?: RedisArgument) { + const args: Array = ['INFO']; if (section) { - args.push(section); + args.push(section); } return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => VerbatimStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/KEYS.spec.ts b/packages/client/lib/commands/KEYS.spec.ts index c066331ea7c..8100559a7e9 100644 --- a/packages/client/lib/commands/KEYS.spec.ts +++ b/packages/client/lib/commands/KEYS.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; describe('KEYS', () => { - testUtils.testWithClient('client.keys', async client => { - assert.deepEqual( - await client.keys('pattern'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('keys', async client => { + assert.deepEqual( + await client.keys('pattern'), + [] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/KEYS.ts b/packages/client/lib/commands/KEYS.ts index c96ee001436..488ba1154c9 100644 --- a/packages/client/lib/commands/KEYS.ts +++ b/packages/client/lib/commands/KEYS.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(pattern: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(pattern: RedisArgument) { return ['KEYS', pattern]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LASTSAVE.spec.ts b/packages/client/lib/commands/LASTSAVE.spec.ts index a6b4863f39e..74cf30705fa 100644 --- a/packages/client/lib/commands/LASTSAVE.spec.ts +++ b/packages/client/lib/commands/LASTSAVE.spec.ts @@ -1,16 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LASTSAVE'; +import LASTSAVE from './LASTSAVE'; describe('LASTSAVE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['LASTSAVE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LASTSAVE.transformArguments(), + ['LASTSAVE'] + ); + }); - testUtils.testWithClient('client.lastSave', async client => { - assert.ok((await client.lastSave()) instanceof Date); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.lastSave', async client => { + assert.equal( + typeof await client.lastSave(), + 'number' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/LASTSAVE.ts b/packages/client/lib/commands/LASTSAVE.ts index 76944d3548b..a65161f78b9 100644 --- a/packages/client/lib/commands/LASTSAVE.ts +++ b/packages/client/lib/commands/LASTSAVE.ts @@ -1,9 +1,10 @@ -export const IS_READ_ONLY = true; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['LASTSAVE']; -} - -export function transformReply(reply: number): Date { - return new Date(reply); -} + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts b/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts index 3888ff8bd36..00eabfb4cb3 100644 --- a/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts +++ b/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts @@ -1,19 +1,19 @@ -import {strict as assert} from 'assert'; -import testUtils, {GLOBAL} from '../test-utils'; -import { transformArguments } from './LATENCY_DOCTOR'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import LATENCY_DOCTOR from './LATENCY_DOCTOR'; describe('LATENCY DOCTOR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['LATENCY', 'DOCTOR'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LATENCY_DOCTOR.transformArguments(), + ['LATENCY', 'DOCTOR'] + ); + }); - testUtils.testWithClient('client.latencyDoctor', async client => { - assert.equal( - typeof (await client.latencyDoctor()), - 'string' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.latencyDoctor', async client => { + assert.equal( + typeof await client.latencyDoctor(), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/LATENCY_DOCTOR.ts b/packages/client/lib/commands/LATENCY_DOCTOR.ts index d2106c06114..96dc6b65702 100644 --- a/packages/client/lib/commands/LATENCY_DOCTOR.ts +++ b/packages/client/lib/commands/LATENCY_DOCTOR.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['LATENCY', 'DOCTOR']; -} +import { BlobStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['LATENCY', 'DOCTOR']; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LATENCY_GRAPH.spec.ts b/packages/client/lib/commands/LATENCY_GRAPH.spec.ts index 21755a253b3..03ad8e2c886 100644 --- a/packages/client/lib/commands/LATENCY_GRAPH.spec.ts +++ b/packages/client/lib/commands/LATENCY_GRAPH.spec.ts @@ -1,28 +1,26 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LATENCY_GRAPH'; +import LATENCY_GRAPH from './LATENCY_GRAPH'; describe('LATENCY GRAPH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('command'), - [ - 'LATENCY', - 'GRAPH', - 'command' - ] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LATENCY_GRAPH.transformArguments('command'), + [ + 'LATENCY', + 'GRAPH', + 'command' + ] + ); + }); - testUtils.testWithClient('client.latencyGraph', async client => { - await Promise.all([ - client.configSet('latency-monitor-threshold', '1'), - client.sendCommand(['DEBUG', 'SLEEP', '0.001']) - ]); + testUtils.testWithClient('client.latencyGraph', async client => { + const [,, reply] = await Promise.all([ + client.configSet('latency-monitor-threshold', '1'), + client.sendCommand(['DEBUG', 'SLEEP', '0.001']), + client.latencyGraph('command') + ]); - assert.equal( - typeof await client.latencyGraph('command'), - 'string' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(typeof reply, 'string'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/LATENCY_GRAPH.ts b/packages/client/lib/commands/LATENCY_GRAPH.ts index e4e078b90f2..7d5f54288b6 100644 --- a/packages/client/lib/commands/LATENCY_GRAPH.ts +++ b/packages/client/lib/commands/LATENCY_GRAPH.ts @@ -1,25 +1,31 @@ -import { RedisCommandArguments } from '.'; +import { BlobStringReply, Command } from '../RESP/types'; -export type EventType = - 'active-defrag-cycle' - | 'aof-fsync-always' - | 'aof-stat' - | 'aof-rewrite-diff-write' - | 'aof-rename' - | 'aof-write' - | 'aof-write-active-child' - | 'aof-write-alone' - | 'aof-write-pending-fsync' - | 'command' - | 'expire-cycle' - | 'eviction-cycle' - | 'eviction-del' - | 'fast-command' - | 'fork' - | 'rdb-unlink-temp-file'; +export const LATENCY_EVENTS = { + ACTIVE_DEFRAG_CYCLE: 'active-defrag-cycle', + AOF_FSYNC_ALWAYS: 'aof-fsync-always', + AOF_STAT: 'aof-stat', + AOF_REWRITE_DIFF_WRITE: 'aof-rewrite-diff-write', + AOF_RENAME: 'aof-rename', + AOF_WRITE: 'aof-write', + AOF_WRITE_ACTIVE_CHILD: 'aof-write-active-child', + AOF_WRITE_ALONE: 'aof-write-alone', + AOF_WRITE_PENDING_FSYNC: 'aof-write-pending-fsync', + COMMAND: 'command', + EXPIRE_CYCLE: 'expire-cycle', + EVICTION_CYCLE: 'eviction-cycle', + EVICTION_DEL: 'eviction-del', + FAST_COMMAND: 'fast-command', + FORK: 'fork', + RDB_UNLINK_TEMP_FILE: 'rdb-unlink-temp-file' +} as const; -export function transformArguments(event: EventType): RedisCommandArguments { - return ['LATENCY', 'GRAPH', event]; -} +export type LatencyEvent = typeof LATENCY_EVENTS[keyof typeof LATENCY_EVENTS]; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(event: LatencyEvent) { + return ['LATENCY', 'GRAPH', event]; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LATENCY_HISTORY.spec.ts b/packages/client/lib/commands/LATENCY_HISTORY.spec.ts index e79e969b261..509c856e28f 100644 --- a/packages/client/lib/commands/LATENCY_HISTORY.spec.ts +++ b/packages/client/lib/commands/LATENCY_HISTORY.spec.ts @@ -1,26 +1,26 @@ -import {strict as assert} from 'assert'; -import testUtils, {GLOBAL} from '../test-utils'; -import { transformArguments } from './LATENCY_HISTORY'; +import { strict as assert } from 'assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import LATENCY_HISTORY from './LATENCY_HISTORY'; describe('LATENCY HISTORY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('command'), - ['LATENCY', 'HISTORY', 'command'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LATENCY_HISTORY.transformArguments('command'), + ['LATENCY', 'HISTORY', 'command'] + ); + }); - testUtils.testWithClient('client.latencyHistory', async client => { - await Promise.all([ - client.configSet('latency-monitor-threshold', '100'), - client.sendCommand(['DEBUG', 'SLEEP', '1']) - ]); - - const latencyHisRes = await client.latencyHistory('command'); - assert.ok(Array.isArray(latencyHisRes)); - for (const [timestamp, latency] of latencyHisRes) { - assert.equal(typeof timestamp, 'number'); - assert.equal(typeof latency, 'number'); - } - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.latencyHistory', async client => { + const [,, reply] = await Promise.all([ + client.configSet('latency-monitor-threshold', '100'), + client.sendCommand(['DEBUG', 'SLEEP', '1']), + client.latencyHistory('command') + ]); + + assert.ok(Array.isArray(reply)); + for (const [timestamp, latency] of reply) { + assert.equal(typeof timestamp, 'number'); + assert.equal(typeof latency, 'number'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/LATENCY_HISTORY.ts b/packages/client/lib/commands/LATENCY_HISTORY.ts index c0b1964553e..df5b1772b2d 100644 --- a/packages/client/lib/commands/LATENCY_HISTORY.ts +++ b/packages/client/lib/commands/LATENCY_HISTORY.ts @@ -1,27 +1,33 @@ -export type EventType = ( - 'active-defrag-cycle' | - 'aof-fsync-always' | - 'aof-stat' | - 'aof-rewrite-diff-write' | - 'aof-rename' | - 'aof-write' | - 'aof-write-active-child' | - 'aof-write-alone' | - 'aof-write-pending-fsync' | - 'command' | - 'expire-cycle' | - 'eviction-cycle' | - 'eviction-del' | - 'fast-command' | - 'fork' | - 'rdb-unlink-temp-file' +import { ArrayReply, TuplesReply, NumberReply, Command } from '../RESP/types'; + +export type LatencyEventType = ( + 'active-defrag-cycle' | + 'aof-fsync-always' | + 'aof-stat' | + 'aof-rewrite-diff-write' | + 'aof-rename' | + 'aof-write' | + 'aof-write-active-child' | + 'aof-write-alone' | + 'aof-write-pending-fsync' | + 'command' | + 'expire-cycle' | + 'eviction-cycle' | + 'eviction-del' | + 'fast-command' | + 'fork' | + 'rdb-unlink-temp-file' ); -export function transformArguments(event: EventType) { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(event: LatencyEventType) { return ['LATENCY', 'HISTORY', event]; -} + }, + transformReply: undefined as unknown as () => ArrayReply> +} as const satisfies Command; -export declare function transformReply(): Array<[ - timestamp: number, - latency: number, -]>; diff --git a/packages/client/lib/commands/LATENCY_LATEST.spec.ts b/packages/client/lib/commands/LATENCY_LATEST.spec.ts index 4087f212139..f85a3ccc8bd 100644 --- a/packages/client/lib/commands/LATENCY_LATEST.spec.ts +++ b/packages/client/lib/commands/LATENCY_LATEST.spec.ts @@ -1,27 +1,27 @@ -import {strict as assert} from 'assert'; -import testUtils, {GLOBAL} from '../test-utils'; -import { transformArguments } from './LATENCY_LATEST'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import LATENCY_LATEST from './LATENCY_LATEST'; describe('LATENCY LATEST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['LATENCY', 'LATEST'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LATENCY_LATEST.transformArguments(), + ['LATENCY', 'LATEST'] + ); + }); - testUtils.testWithClient('client.latencyLatest', async client => { - await Promise.all([ - client.configSet('latency-monitor-threshold', '100'), - client.sendCommand(['DEBUG', 'SLEEP', '1']) - ]); - const latency = await client.latencyLatest(); - assert.ok(Array.isArray(latency)); - for (const [name, timestamp, latestLatency, allTimeLatency] of latency) { - assert.equal(typeof name, 'string'); - assert.equal(typeof timestamp, 'number'); - assert.equal(typeof latestLatency, 'number'); - assert.equal(typeof allTimeLatency, 'number'); - } - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.latencyLatest', async client => { + const [,, reply] = await Promise.all([ + client.configSet('latency-monitor-threshold', '100'), + client.sendCommand(['DEBUG', 'SLEEP', '1']), + client.latencyLatest() + ]); + assert.ok(Array.isArray(reply)); + for (const [name, timestamp, latestLatency, allTimeLatency] of reply) { + assert.equal(typeof name, 'string'); + assert.equal(typeof timestamp, 'number'); + assert.equal(typeof latestLatency, 'number'); + assert.equal(typeof allTimeLatency, 'number'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/LATENCY_LATEST.ts b/packages/client/lib/commands/LATENCY_LATEST.ts index 3e4dd6236c6..29548af30dc 100644 --- a/packages/client/lib/commands/LATENCY_LATEST.ts +++ b/packages/client/lib/commands/LATENCY_LATEST.ts @@ -1,12 +1,16 @@ -import { RedisCommandArguments } from '.'; +import { ArrayReply, BlobStringReply, NumberReply, Command } from '../RESP/types'; -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['LATENCY', 'LATEST']; -} + }, + transformReply: undefined as unknown as () => ArrayReply<[ + name: BlobStringReply, + timestamp: NumberReply, + latestLatency: NumberReply, + allTimeLatency: NumberReply + ]> +} as const satisfies Command; -export declare function transformReply(): Array<[ - name: string, - timestamp: number, - latestLatency: number, - allTimeLatency: number -]>; diff --git a/packages/client/lib/commands/LCS.spec.ts b/packages/client/lib/commands/LCS.spec.ts index a4d9035571e..ff9d63db1b1 100644 --- a/packages/client/lib/commands/LCS.spec.ts +++ b/packages/client/lib/commands/LCS.spec.ts @@ -1,28 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LCS'; +import LCS from './LCS'; describe('LCS', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('1', '2'), - ['LCS', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LCS.transformArguments('1', '2'), + ['LCS', '1', '2'] + ); + }); - testUtils.testWithClient('client.lcs', async client => { - assert.equal( - await client.lcs('1', '2'), - '' - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lcs', async cluster => { - assert.equal( - await cluster.lcs('{tag}1', '{tag}2'), - '' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lcs', async client => { + assert.equal( + await client.lcs('{tag}1', '{tag}2'), + '' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LCS.ts b/packages/client/lib/commands/LCS.ts index b075b73e8a8..b798f12aa37 100644 --- a/packages/client/lib/commands/LCS.ts +++ b/packages/client/lib/commands/LCS.ts @@ -1,18 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key1: RedisCommandArgument, - key2: RedisCommandArgument -): RedisCommandArguments { - return [ - 'LCS', - key1, - key2 - ]; -} - -export declare function transformReply(): string | Buffer; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key1: RedisArgument, + key2: RedisArgument + ) { + return ['LCS', key1, key2]; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LCS_IDX.spec.ts b/packages/client/lib/commands/LCS_IDX.spec.ts index fc3ee54f7c0..2f60a205faa 100644 --- a/packages/client/lib/commands/LCS_IDX.spec.ts +++ b/packages/client/lib/commands/LCS_IDX.spec.ts @@ -1,41 +1,34 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LCS_IDX'; +import LCS_IDX from './LCS_IDX'; -describe('LCS_IDX', () => { - testUtils.isVersionGreaterThanHook([7]); +describe('LCS IDX', () => { + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('1', '2'), - ['LCS', '1', '2', 'IDX'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LCS_IDX.transformArguments('1', '2'), + ['LCS', '1', '2', 'IDX'] + ); + }); - testUtils.testWithClient('client.lcsIdx', async client => { - const [, reply] = await Promise.all([ - client.mSet({ - '1': 'abc', - '2': 'bc' - }), - client.lcsIdx('1', '2') - ]); + testUtils.testWithClient('client.lcsIdx', async client => { + const [, reply] = await Promise.all([ + client.mSet({ + '1': 'abc', + '2': 'bc' + }), + client.lcsIdx('1', '2') + ]); - assert.deepEqual( - reply, - { - matches: [{ - key1: { - start: 1, - end: 2 - }, - key2: { - start: 0, - end: 1 - } - }], - length: 2 - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + reply, + { + matches: [ + [[1, 2], [0, 1]] + ], + len: 2 + } + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/LCS_IDX.ts b/packages/client/lib/commands/LCS_IDX.ts index 262a02ba4c6..0c266fffe1c 100644 --- a/packages/client/lib/commands/LCS_IDX.ts +++ b/packages/client/lib/commands/LCS_IDX.ts @@ -1,42 +1,50 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { RangeReply, RawRangeReply, transformRangeReply } from './generic-transformers'; -import { transformArguments as transformLcsArguments } from './LCS'; +import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, NumberReply, UnwrapReply, Resp2Reply, Command, TuplesReply } from '../RESP/types'; +import LCS from './LCS'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LCS'; +export interface LcsIdxOptions { + MINMATCHLEN?: number; +} + +export type LcsIdxRange = TuplesReply<[ + start: NumberReply, + end: NumberReply +]>; + +export type LcsIdxMatches = ArrayReply< + TuplesReply<[ + key1: LcsIdxRange, + key2: LcsIdxRange + ]> +>; + +export type LcsIdxReply = TuplesToMapReply<[ + [BlobStringReply<'matches'>, LcsIdxMatches], + [BlobStringReply<'len'>, NumberReply] +]>; + +export default { + FIRST_KEY_INDEX: LCS.FIRST_KEY_INDEX, + IS_READ_ONLY: LCS.IS_READ_ONLY, + transformArguments( + key1: RedisArgument, + key2: RedisArgument, + options?: LcsIdxOptions + ) { + const args = LCS.transformArguments(key1, key2); -export function transformArguments( - key1: RedisCommandArgument, - key2: RedisCommandArgument -): RedisCommandArguments { - const args = transformLcsArguments(key1, key2); args.push('IDX'); - return args; -} -type RawReply = [ - 'matches', - Array<[ - key1: RawRangeReply, - key2: RawRangeReply - ]>, - 'len', - number -]; - -interface Reply { - matches: Array<{ - key1: RangeReply; - key2: RangeReply; - }>; - length: number; -} + if (options?.MINMATCHLEN) { + args.push('MINMATCHLEN', options.MINMATCHLEN.toString()); + } -export function transformReply(reply: RawReply): Reply { - return { - matches: reply[1].map(([key1, key2]) => ({ - key1: transformRangeReply(key1), - key2: transformRangeReply(key2) - })), - length: reply[3] - }; -} + return args; + }, + transformReply: { + 2: (reply: UnwrapReply>) => ({ + matches: reply[1], + len: reply[3] + }), + 3: undefined as unknown as () => LcsIdxReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts index 8be9b993135..39ba17e8f2c 100644 --- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts +++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts @@ -1,42 +1,34 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LCS_IDX_WITHMATCHLEN'; +import LCS_IDX_WITHMATCHLEN from './LCS_IDX_WITHMATCHLEN'; -describe('LCS_IDX_WITHMATCHLEN', () => { - testUtils.isVersionGreaterThanHook([7]); +describe('LCS IDX WITHMATCHLEN', () => { + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('1', '2'), - ['LCS', '1', '2', 'IDX', 'WITHMATCHLEN'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LCS_IDX_WITHMATCHLEN.transformArguments('1', '2'), + ['LCS', '1', '2', 'IDX', 'WITHMATCHLEN'] + ); + }); - testUtils.testWithClient('client.lcsIdxWithMatchLen', async client => { - const [, reply] = await Promise.all([ - client.mSet({ - '1': 'abc', - '2': 'bc' - }), - client.lcsIdxWithMatchLen('1', '2') - ]); + testUtils.testWithClient('client.lcsIdxWithMatchLen', async client => { + const [, reply] = await Promise.all([ + client.mSet({ + '1': 'abc', + '2': 'bc' + }), + client.lcsIdxWithMatchLen('1', '2') + ]); - assert.deepEqual( - reply, - { - matches: [{ - key1: { - start: 1, - end: 2 - }, - key2: { - start: 0, - end: 1 - }, - length: 2 - }], - length: 2 - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + reply, + { + matches: [ + [[1, 2], [0, 1], 2] + ], + len: 2 + } + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts index 989870d6ca2..4e645852035 100644 --- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts +++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts @@ -1,45 +1,36 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { RangeReply, RawRangeReply, transformRangeReply } from './generic-transformers'; -import { transformArguments as transformLcsArguments } from './LCS'; +import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, TuplesReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; +import LCS_IDX, { LcsIdxOptions, LcsIdxRange } from './LCS_IDX'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LCS'; +export type LcsIdxWithMatchLenMatches = ArrayReply< + TuplesReply<[ + key1: LcsIdxRange, + key2: LcsIdxRange, + len: NumberReply + ]> +>; -export function transformArguments( - key1: RedisCommandArgument, - key2: RedisCommandArgument -): RedisCommandArguments { - const args = transformLcsArguments(key1, key2); - args.push('IDX', 'WITHMATCHLEN'); - return args; -} - -type RawReply = [ - 'matches', - Array<[ - key1: RawRangeReply, - key2: RawRangeReply, - length: number - ]>, - 'len', - number -]; +export type LcsIdxWithMatchLenReply = TuplesToMapReply<[ + [BlobStringReply<'matches'>, LcsIdxWithMatchLenMatches], + [BlobStringReply<'len'>, NumberReply] +]>; -interface Reply { - matches: Array<{ - key1: RangeReply; - key2: RangeReply; - length: number; - }>; - length: number; -} - -export function transformReply(reply: RawReply): Reply { - return { - matches: reply[1].map(([key1, key2, length]) => ({ - key1: transformRangeReply(key1), - key2: transformRangeReply(key2), - length - })), - length: reply[3] - }; -} +export default { + FIRST_KEY_INDEX: LCS_IDX.FIRST_KEY_INDEX, + IS_READ_ONLY: LCS_IDX.IS_READ_ONLY, + transformArguments( + key1: RedisArgument, + key2: RedisArgument, + options?: LcsIdxOptions + ) { + const args = LCS_IDX.transformArguments(key1, key2); + args.push('WITHMATCHLEN'); + return args; + }, + transformReply: { + 2: (reply: UnwrapReply>) => ({ + matches: reply[1], + len: reply[3] + }), + 3: undefined as unknown as () => LcsIdxWithMatchLenReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/LCS_LEN.spec.ts b/packages/client/lib/commands/LCS_LEN.spec.ts index bf4eefd3301..9dc163a68a9 100644 --- a/packages/client/lib/commands/LCS_LEN.spec.ts +++ b/packages/client/lib/commands/LCS_LEN.spec.ts @@ -1,28 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LCS_LEN'; +import LCS_LEN from './LCS_LEN'; describe('LCS_LEN', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('1', '2'), - ['LCS', '1', '2', 'LEN'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LCS_LEN.transformArguments('1', '2'), + ['LCS', '1', '2', 'LEN'] + ); + }); - testUtils.testWithClient('client.lcsLen', async client => { - assert.equal( - await client.lcsLen('1', '2'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lcsLen', async cluster => { - assert.equal( - await cluster.lcsLen('{tag}1', '{tag}2'), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lcsLen', async client => { + assert.equal( + await client.lcsLen('{tag}1', '{tag}2'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LCS_LEN.ts b/packages/client/lib/commands/LCS_LEN.ts index a5121e4c13f..d5d0e77e4d6 100644 --- a/packages/client/lib/commands/LCS_LEN.ts +++ b/packages/client/lib/commands/LCS_LEN.ts @@ -1,15 +1,16 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformLcsArguments } from './LCS'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import LCS from './LCS'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LCS'; - -export function transformArguments( - key1: RedisCommandArgument, - key2: RedisCommandArgument -): RedisCommandArguments { - const args = transformLcsArguments(key1, key2); +export default { + FIRST_KEY_INDEX: LCS.FIRST_KEY_INDEX, + IS_READ_ONLY: LCS.IS_READ_ONLY, + transformArguments( + key1: RedisArgument, + key2: RedisArgument + ) { + const args = LCS.transformArguments(key1, key2); args.push('LEN'); return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LINDEX.spec.ts b/packages/client/lib/commands/LINDEX.spec.ts index aa3aafa789b..60346b85f21 100644 --- a/packages/client/lib/commands/LINDEX.spec.ts +++ b/packages/client/lib/commands/LINDEX.spec.ts @@ -1,36 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LINDEX'; -describe('LINDEX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0), - ['LINDEX', 'key', '0'] - ); - }); - - describe('client.lIndex', () => { - testUtils.testWithClient('null', async client => { - assert.equal( - await client.lIndex('key', 0), - null - ); - }, GLOBAL.SERVERS.OPEN); +import LINDEX from './LINDEX'; - testUtils.testWithClient('with value', async client => { - const [, lIndexReply] = await Promise.all([ - client.lPush('key', 'element'), - client.lIndex('key', 0) - ]); - - assert.equal(lIndexReply, 'element'); - }, GLOBAL.SERVERS.OPEN); - }); +describe('LINDEX', () => { + it('transformArguments', () => { + assert.deepEqual( + LINDEX.transformArguments('key', 0), + ['LINDEX', 'key', '0'] + ); + }); - testUtils.testWithCluster('cluster.lIndex', async cluster => { - assert.equal( - await cluster.lIndex('key', 0), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lIndex', async client => { + assert.equal( + await client.lIndex('key', 0), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); \ No newline at end of file diff --git a/packages/client/lib/commands/LINDEX.ts b/packages/client/lib/commands/LINDEX.ts index 8e74ad8aae6..0478bf9dc41 100644 --- a/packages/client/lib/commands/LINDEX.ts +++ b/packages/client/lib/commands/LINDEX.ts @@ -1,14 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - index: number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, index: number) { return ['LINDEX', key, index.toString()]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LINSERT.spec.ts b/packages/client/lib/commands/LINSERT.spec.ts index 6454cc48536..6cafa3f5a84 100644 --- a/packages/client/lib/commands/LINSERT.spec.ts +++ b/packages/client/lib/commands/LINSERT.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LINSERT'; +import LINSERT from './LINSERT'; describe('LINSERT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'BEFORE', 'pivot', 'element'), - ['LINSERT', 'key', 'BEFORE', 'pivot', 'element'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LINSERT.transformArguments('key', 'BEFORE', 'pivot', 'element'), + ['LINSERT', 'key', 'BEFORE', 'pivot', 'element'] + ); + }); - testUtils.testWithClient('client.lInsert', async client => { - assert.equal( - await client.lInsert('key', 'BEFORE', 'pivot', 'element'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lInsert', async cluster => { - assert.equal( - await cluster.lInsert('key', 'BEFORE', 'pivot', 'element'), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lInsert', async client => { + assert.equal( + await client.lInsert('key', 'BEFORE', 'pivot', 'element'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LINSERT.ts b/packages/client/lib/commands/LINSERT.ts index 0a8e1f32ba4..4bdc77de5a4 100644 --- a/packages/client/lib/commands/LINSERT.ts +++ b/packages/client/lib/commands/LINSERT.ts @@ -1,22 +1,23 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; type LInsertPosition = 'BEFORE' | 'AFTER'; -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, position: LInsertPosition, - pivot: RedisCommandArgument, - element: RedisCommandArgument -): RedisCommandArguments { + pivot: RedisArgument, + element: RedisArgument + ) { return [ - 'LINSERT', - key, - position, - pivot, - element + 'LINSERT', + key, + position, + pivot, + element ]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LLEN.spec.ts b/packages/client/lib/commands/LLEN.spec.ts index fb126ddad55..f6ac9a73cc3 100644 --- a/packages/client/lib/commands/LLEN.spec.ts +++ b/packages/client/lib/commands/LLEN.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LLEN'; +import LLEN from './LLEN'; describe('LLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['LLEN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LLEN.transformArguments('key'), + ['LLEN', 'key'] + ); + }); - testUtils.testWithClient('client.lLen', async client => { - assert.equal( - await client.lLen('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lLen', async cluster => { - assert.equal( - await cluster.lLen('key'), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lLen', async client => { + assert.equal( + await client.lLen('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LLEN.ts b/packages/client/lib/commands/LLEN.ts index 3410e57d424..dda59ddf291 100644 --- a/packages/client/lib/commands/LLEN.ts +++ b/packages/client/lib/commands/LLEN.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['LLEN', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LMOVE.spec.ts b/packages/client/lib/commands/LMOVE.spec.ts index f1d418c394e..86740aa7b77 100644 --- a/packages/client/lib/commands/LMOVE.spec.ts +++ b/packages/client/lib/commands/LMOVE.spec.ts @@ -1,28 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LMOVE'; +import LMOVE from './LMOVE'; describe('LMOVE', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination', 'LEFT', 'RIGHT'), - ['LMOVE', 'source', 'destination', 'LEFT', 'RIGHT'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LMOVE.transformArguments('source', 'destination', 'LEFT', 'RIGHT'), + ['LMOVE', 'source', 'destination', 'LEFT', 'RIGHT'] + ); + }); - testUtils.testWithClient('client.lMove', async client => { - assert.equal( - await client.lMove('source', 'destination', 'LEFT', 'RIGHT'), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lMove', async cluster => { - assert.equal( - await cluster.lMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT'), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lMove', async client => { + assert.equal( + await client.lMove('{tag}source', '{tag}destination', 'LEFT', 'RIGHT'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LMOVE.ts b/packages/client/lib/commands/LMOVE.ts index 849c6385f5a..95adc71a0ff 100644 --- a/packages/client/lib/commands/LMOVE.ts +++ b/packages/client/lib/commands/LMOVE.ts @@ -1,21 +1,22 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; import { ListSide } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + source: RedisArgument, + destination: RedisArgument, sourceSide: ListSide, destinationSide: ListSide -): RedisCommandArguments { + ) { return [ - 'LMOVE', - source, - destination, - sourceSide, - destinationSide, + 'LMOVE', + source, + destination, + sourceSide, + destinationSide, ]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LMPOP.spec.ts b/packages/client/lib/commands/LMPOP.spec.ts index 5675ee9a285..faf39e053ef 100644 --- a/packages/client/lib/commands/LMPOP.spec.ts +++ b/packages/client/lib/commands/LMPOP.spec.ts @@ -1,32 +1,50 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LMPOP'; +import LMPOP from './LMPOP'; describe('LMPOP', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 'LEFT'), - ['LMPOP', '1', 'key', 'LEFT'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + LMPOP.transformArguments('key', 'LEFT'), + ['LMPOP', '1', 'key', 'LEFT'] + ); + }); - it('with COUNT', () => { - assert.deepEqual( - transformArguments('key', 'LEFT', { - COUNT: 2 - }), - ['LMPOP', '1', 'key', 'LEFT', 'COUNT', '2'] - ); - }); + it('with COUNT', () => { + assert.deepEqual( + LMPOP.transformArguments('key', 'LEFT', { + COUNT: 2 + }), + ['LMPOP', '1', 'key', 'LEFT', 'COUNT', '2'] + ); }); + }); + + testUtils.testAll('lmPop - null', async client => { + assert.equal( + await client.lmPop('key', 'RIGHT'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); + + testUtils.testAll('lmPop - with member', async client => { + const [, reply] = await Promise.all([ + client.lPush('key', 'element'), + client.lmPop('key', 'RIGHT') + ]); - testUtils.testWithClient('client.lmPop', async client => { - assert.deepEqual( - await client.lmPop('key', 'RIGHT'), - null - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [ + 'key', + ['element'] + ]); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LMPOP.ts b/packages/client/lib/commands/LMPOP.ts index 29d868b982f..49f7272ec46 100644 --- a/packages/client/lib/commands/LMPOP.ts +++ b/packages/client/lib/commands/LMPOP.ts @@ -1,22 +1,37 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformLMPopArguments, LMPopOptions, ListSide } from './generic-transformers'; +import { CommandArguments, NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types'; +import { ListSide, RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 2; +export interface LMPopOptions { + COUNT?: number; +} + +export function transformLMPopArguments( + args: CommandArguments, + keys: RedisVariadicArgument, + side: ListSide, + options?: LMPopOptions +): CommandArguments { + args = pushVariadicArgument(args, keys); + + args.push(side); -export function transformArguments( - keys: RedisCommandArgument | Array, - side: ListSide, - options?: LMPopOptions -): RedisCommandArguments { - return transformLMPopArguments( - ['LMPOP'], - keys, - side, - options - ); + if (options?.COUNT !== undefined) { + args.push('COUNT', options.COUNT.toString()); + } + + return args; } -export declare function transformReply(): null | [ - key: string, - elements: Array -]; +export type LMPopArguments = typeof transformLMPopArguments extends (_: any, ...args: infer T) => any ? T : never; + +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments(...args: LMPopArguments) { + return transformLMPopArguments(['LMPOP'], ...args); + }, + transformReply: undefined as unknown as () => NullReply | TuplesReply<[ + key: BlobStringReply, + elements: Array + ]> +} as const satisfies Command; diff --git a/packages/client/lib/commands/LOLWUT.spec.ts b/packages/client/lib/commands/LOLWUT.spec.ts index db335893302..b05c4168f6f 100644 --- a/packages/client/lib/commands/LOLWUT.spec.ts +++ b/packages/client/lib/commands/LOLWUT.spec.ts @@ -1,35 +1,35 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LOLWUT'; +import LOLWUT from './LOLWUT'; describe('LOLWUT', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['LOLWUT'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + LOLWUT.transformArguments(), + ['LOLWUT'] + ); + }); - it('with version', () => { - assert.deepEqual( - transformArguments(5), - ['LOLWUT', 'VERSION', '5'] - ); - }); + it('with version', () => { + assert.deepEqual( + LOLWUT.transformArguments(5), + ['LOLWUT', 'VERSION', '5'] + ); + }); - it('with version and optional arguments', () => { - assert.deepEqual( - transformArguments(5, 1, 2, 3), - ['LOLWUT', 'VERSION', '5', '1', '2', '3'] - ); - }); + it('with version and optional arguments', () => { + assert.deepEqual( + LOLWUT.transformArguments(5, 1, 2, 3), + ['LOLWUT', 'VERSION', '5', '1', '2', '3'] + ); }); + }); - testUtils.testWithClient('client.LOLWUT', async client => { - assert.equal( - typeof (await client.LOLWUT()), - 'string' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.LOLWUT', async client => { + assert.equal( + typeof (await client.LOLWUT()), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/LOLWUT.ts b/packages/client/lib/commands/LOLWUT.ts index 5d5fc726065..7a6c8329d69 100644 --- a/packages/client/lib/commands/LOLWUT.ts +++ b/packages/client/lib/commands/LOLWUT.ts @@ -1,19 +1,20 @@ -import { RedisCommandArgument } from '.'; +import { BlobStringReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(version?: number, ...optionalArguments: Array): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(version?: number, ...optionalArguments: Array) { const args = ['LOLWUT']; if (version) { - args.push( - 'VERSION', - version.toString(), - ...optionalArguments.map(String), - ); + args.push( + 'VERSION', + version.toString(), + ...optionalArguments.map(String), + ); } return args; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LPOP.spec.ts b/packages/client/lib/commands/LPOP.spec.ts index d694fb10588..944e559b15f 100644 --- a/packages/client/lib/commands/LPOP.spec.ts +++ b/packages/client/lib/commands/LPOP.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LPOP'; +import LPOP from './LPOP'; describe('LPOP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['LPOP', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LPOP.transformArguments('key'), + ['LPOP', 'key'] + ); + }); - testUtils.testWithClient('client.lPop', async client => { - assert.equal( - await client.lPop('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lPop', async cluster => { - assert.equal( - await cluster.lPop('key'), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lPop', async client => { + assert.equal( + await client.lPop('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LPOP.ts b/packages/client/lib/commands/LPOP.ts index 5dd1bea5196..7c85c30f9a1 100644 --- a/packages/client/lib/commands/LPOP.ts +++ b/packages/client/lib/commands/LPOP.ts @@ -1,9 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument) { return ['LPOP', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LPOP_COUNT.spec.ts b/packages/client/lib/commands/LPOP_COUNT.spec.ts index 9d87fad3862..8286a504428 100644 --- a/packages/client/lib/commands/LPOP_COUNT.spec.ts +++ b/packages/client/lib/commands/LPOP_COUNT.spec.ts @@ -1,28 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LPOP_COUNT'; +import LPOP_COUNT from './LPOP_COUNT'; describe('LPOP COUNT', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['LPOP', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LPOP_COUNT.transformArguments('key', 1), + ['LPOP', 'key', '1'] + ); + }); - testUtils.testWithClient('client.lPopCount', async client => { - assert.equal( - await client.lPopCount('key', 1), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lPopCount', async cluster => { - assert.equal( - await cluster.lPopCount('key', 1), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lPopCount', async client => { + assert.equal( + await client.lPopCount('key', 1), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LPOP_COUNT.ts b/packages/client/lib/commands/LPOP_COUNT.ts index 021517b018a..a1536e78dcb 100644 --- a/packages/client/lib/commands/LPOP_COUNT.ts +++ b/packages/client/lib/commands/LPOP_COUNT.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NullReply, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, count: number) { return ['LPOP', key, count.toString()]; -} - -export declare function transformReply(): Array | null; + }, + transformReply: undefined as unknown as () => NullReply | ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LPOS.spec.ts b/packages/client/lib/commands/LPOS.spec.ts index 6b6050f2c3b..94c0bb3c034 100644 --- a/packages/client/lib/commands/LPOS.spec.ts +++ b/packages/client/lib/commands/LPOS.spec.ts @@ -1,58 +1,54 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LPOS'; +import LPOS from './LPOS'; describe('LPOS', () => { - testUtils.isVersionGreaterThanHook([6, 0, 6]); + testUtils.isVersionGreaterThanHook([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'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + LPOS.transformArguments('key', 'element'), + ['LPOS', 'key', 'element'] + ); + }); - it('with MAXLEN', () => { - assert.deepEqual( - transformArguments('key', 'element', { - MAXLEN: 10 - }), - ['LPOS', 'key', 'element', 'MAXLEN', '10'] - ); - }); + it('with RANK', () => { + assert.deepEqual( + LPOS.transformArguments('key', 'element', { + RANK: 0 + }), + ['LPOS', 'key', 'element', 'RANK', '0'] + ); + }); - it('with RANK, MAXLEN', () => { - assert.deepEqual( - transformArguments('key', 'element', { - RANK: 0, - MAXLEN: 10 - }), - ['LPOS', 'key', 'element', 'RANK', '0', 'MAXLEN', '10'] - ); - }); + it('with MAXLEN', () => { + assert.deepEqual( + LPOS.transformArguments('key', 'element', { + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'MAXLEN', '10'] + ); }); - testUtils.testWithClient('client.lPos', async client => { - assert.equal( - await client.lPos('key', 'element'), - null - ); - }, GLOBAL.SERVERS.OPEN); + it('with RANK, MAXLEN', () => { + assert.deepEqual( + LPOS.transformArguments('key', 'element', { + RANK: 0, + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'RANK', '0', 'MAXLEN', '10'] + ); + }); + }); - testUtils.testWithCluster('cluster.lPos', async cluster => { - assert.equal( - await cluster.lPos('key', 'element'), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lPos', async client => { + assert.equal( + await client.lPos('key', 'element'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LPOS.ts b/packages/client/lib/commands/LPOS.ts index 1f2e34ab88e..047d80d7c29 100644 --- a/packages/client/lib/commands/LPOS.ts +++ b/packages/client/lib/commands/LPOS.ts @@ -1,30 +1,31 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export interface LPosOptions { - RANK?: number; - MAXLEN?: number; + RANK?: number; + MAXLEN?: number; } -export function transformArguments( - key: RedisCommandArgument, - element: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + element: RedisArgument, options?: LPosOptions -): RedisCommandArguments { + ) { const args = ['LPOS', key, element]; - if (typeof options?.RANK === 'number') { + if (options) { + if (typeof options.RANK === 'number') { args.push('RANK', options.RANK.toString()); - } + } - if (typeof options?.MAXLEN === 'number') { + if (typeof options.MAXLEN === 'number') { args.push('MAXLEN', options.MAXLEN.toString()); + } } return args; -} - -export declare function transformReply(): number | null; + }, + transformReply: undefined as unknown as () => NumberReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LPOS_COUNT.spec.ts b/packages/client/lib/commands/LPOS_COUNT.spec.ts index 4b01f2f59b9..747ffbc01cf 100644 --- a/packages/client/lib/commands/LPOS_COUNT.spec.ts +++ b/packages/client/lib/commands/LPOS_COUNT.spec.ts @@ -1,58 +1,54 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LPOS_COUNT'; +import LPOS_COUNT from './LPOS_COUNT'; describe('LPOS COUNT', () => { - testUtils.isVersionGreaterThanHook([6, 0, 6]); + testUtils.isVersionGreaterThanHook([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'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + LPOS_COUNT.transformArguments('key', 'element', 0), + ['LPOS', 'key', 'element', 'COUNT', '0'] + ); + }); - it('with MAXLEN', () => { - assert.deepEqual( - transformArguments('key', 'element', 0, { - MAXLEN: 10 - }), - ['LPOS', 'key', 'element', 'COUNT', '0', 'MAXLEN', '10'] - ); - }); + it('with RANK', () => { + assert.deepEqual( + LPOS_COUNT.transformArguments('key', 'element', 0, { + RANK: 0 + }), + ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0'] + ); + }); - it('with RANK, MAXLEN', () => { - assert.deepEqual( - transformArguments('key', 'element', 0, { - RANK: 0, - MAXLEN: 10 - }), - ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0', 'MAXLEN', '10'] - ); - }); + it('with MAXLEN', () => { + assert.deepEqual( + LPOS_COUNT.transformArguments('key', 'element', 0, { + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'COUNT', '0', 'MAXLEN', '10'] + ); }); - testUtils.testWithClient('client.lPosCount', async client => { - assert.deepEqual( - await client.lPosCount('key', 'element', 0), - [] - ); - }, GLOBAL.SERVERS.OPEN); + it('with RANK, MAXLEN', () => { + assert.deepEqual( + LPOS_COUNT.transformArguments('key', 'element', 0, { + RANK: 0, + MAXLEN: 10 + }), + ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0', 'MAXLEN', '10'] + ); + }); + }); - testUtils.testWithCluster('cluster.lPosCount', async cluster => { - assert.deepEqual( - await cluster.lPosCount('key', 'element', 0), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lPosCount', async client => { + assert.deepEqual( + await client.lPosCount('key', 'element', 0), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LPOS_COUNT.ts b/packages/client/lib/commands/LPOS_COUNT.ts index 0549df82db5..1b057cff1f6 100644 --- a/packages/client/lib/commands/LPOS_COUNT.ts +++ b/packages/client/lib/commands/LPOS_COUNT.ts @@ -1,27 +1,28 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { LPosOptions } from './LPOS'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; +import LPOS, { LPosOptions } from './LPOS'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './LPOS'; - -export function transformArguments( - key: RedisCommandArgument, - element: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: LPOS.FIRST_KEY_INDEX, + IS_READ_ONLY: LPOS.IS_READ_ONLY, + transformArguments( + key: RedisArgument, + element: RedisArgument, count: number, options?: LPosOptions -): RedisCommandArguments { + ) { const args = ['LPOS', key, element]; - if (typeof options?.RANK === 'number') { - args.push('RANK', options.RANK.toString()); + if (options?.RANK !== undefined) { + args.push('RANK', options.RANK.toString()); } args.push('COUNT', count.toString()); - if (typeof options?.MAXLEN === 'number') { - args.push('MAXLEN', options.MAXLEN.toString()); + if (options?.MAXLEN !== undefined) { + args.push('MAXLEN', options.MAXLEN.toString()); } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LPUSH.spec.ts b/packages/client/lib/commands/LPUSH.spec.ts index b5b1f5084eb..8d2ddb5ccc2 100644 --- a/packages/client/lib/commands/LPUSH.spec.ts +++ b/packages/client/lib/commands/LPUSH.spec.ts @@ -1,35 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LPUSH'; +import LPUSH 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'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + LPUSH.transformArguments('key', 'field'), + ['LPUSH', 'key', 'field'] + ); }); - testUtils.testWithClient('client.lPush', async client => { - assert.equal( - await client.lPush('key', 'field'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + it('array', () => { + assert.deepEqual( + LPUSH.transformArguments('key', ['1', '2']), + ['LPUSH', 'key', '1', '2'] + ); + }); + }); - testUtils.testWithCluster('cluster.lPush', async cluster => { - assert.equal( - await cluster.lPush('key', 'field'), - 1 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lPush', async client => { + assert.equal( + await client.lPush('key', 'field'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LPUSH.ts b/packages/client/lib/commands/LPUSH.ts index 7144b146e27..714db2ae9a9 100644 --- a/packages/client/lib/commands/LPUSH.ts +++ b/packages/client/lib/commands/LPUSH.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - elements: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['LPUSH', key], elements);} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, elements: RedisVariadicArgument) { + return pushVariadicArguments(['LPUSH', key], elements); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LPUSHX.spec.ts b/packages/client/lib/commands/LPUSHX.spec.ts index d978e5a588f..f7dafe025c7 100644 --- a/packages/client/lib/commands/LPUSHX.spec.ts +++ b/packages/client/lib/commands/LPUSHX.spec.ts @@ -1,35 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LPUSHX'; +import LPUSHX 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'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + LPUSHX.transformArguments('key', 'element'), + ['LPUSHX', 'key', 'element'] + ); }); - testUtils.testWithClient('client.lPushX', async client => { - assert.equal( - await client.lPushX('key', 'element'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + it('array', () => { + assert.deepEqual( + LPUSHX.transformArguments('key', ['1', '2']), + ['LPUSHX', 'key', '1', '2'] + ); + }); + }); - testUtils.testWithCluster('cluster.lPushX', async cluster => { - assert.equal( - await cluster.lPushX('key', 'element'), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lPushX', async client => { + assert.equal( + await client.lPushX('key', 'element'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LPUSHX.ts b/packages/client/lib/commands/LPUSHX.ts index 0b518add6da..d6dceda6d02 100644 --- a/packages/client/lib/commands/LPUSHX.ts +++ b/packages/client/lib/commands/LPUSHX.ts @@ -1,13 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - element: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['LPUSHX', key], element); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, elements: RedisVariadicArgument) { + return pushVariadicArguments(['LPUSHX', key], elements); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LRANGE.spec.ts b/packages/client/lib/commands/LRANGE.spec.ts index dffe6087b80..3e768cc0731 100644 --- a/packages/client/lib/commands/LRANGE.spec.ts +++ b/packages/client/lib/commands/LRANGE.spec.ts @@ -1,27 +1,22 @@ - -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LRANGE'; +import LRANGE from './LRANGE'; describe('LRANGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, -1), - ['LRANGE', 'key', '0', '-1'] - ); - }); - - testUtils.testWithClient('client.lRange', async client => { - assert.deepEqual( - await client.lRange('key', 0, -1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + it('transformArguments', () => { + assert.deepEqual( + LRANGE.transformArguments('key', 0, -1), + ['LRANGE', 'key', '0', '-1'] + ); + }); - testUtils.testWithCluster('cluster.lRange', async cluster => { - assert.deepEqual( - await cluster.lRange('key', 0, -1), - [] - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lRange', async client => { + assert.deepEqual( + await client.lRange('key', 0, -1), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LRANGE.ts b/packages/client/lib/commands/LRANGE.ts index df12c57d804..4b4b3488baa 100644 --- a/packages/client/lib/commands/LRANGE.ts +++ b/packages/client/lib/commands/LRANGE.ts @@ -1,20 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, start: number, stop: number -): RedisCommandArguments { +) { return [ - 'LRANGE', - key, - start.toString(), - stop.toString() + 'LRANGE', + key, + start.toString(), + stop.toString() ]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LREM.spec.ts b/packages/client/lib/commands/LREM.spec.ts index 3405f4beb07..1a35dab7c54 100644 --- a/packages/client/lib/commands/LREM.spec.ts +++ b/packages/client/lib/commands/LREM.spec.ts @@ -1,27 +1,22 @@ - -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LREM'; +import LREM from './LREM'; describe('LREM', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 'element'), - ['LREM', 'key', '0', 'element'] - ); - }); - - testUtils.testWithClient('client.lRem', async client => { - assert.equal( - await client.lRem('key', 0, 'element'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + it('transformArguments', () => { + assert.deepEqual( + LREM.transformArguments('key', 0, 'element'), + ['LREM', 'key', '0', 'element'] + ); + }); - testUtils.testWithCluster('cluster.lRem', async cluster => { - assert.equal( - await cluster.lRem('key', 0, 'element'), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lRem', async client => { + assert.equal( + await client.lRem('key', 0, 'element'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LREM.ts b/packages/client/lib/commands/LREM.ts index b4951334888..dbd2002be8b 100644 --- a/packages/client/lib/commands/LREM.ts +++ b/packages/client/lib/commands/LREM.ts @@ -1,18 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, count: number, - element: RedisCommandArgument -): RedisCommandArguments { + element: RedisArgument + ) { return [ - 'LREM', - key, - count.toString(), - element + 'LREM', + key, + count.toString(), + element ]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/LSET.spec.ts b/packages/client/lib/commands/LSET.spec.ts index d7241032cc6..ae4b1bf9f2f 100644 --- a/packages/client/lib/commands/LSET.spec.ts +++ b/packages/client/lib/commands/LSET.spec.ts @@ -1,28 +1,23 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LSET'; +import LSET from './LSET'; describe('LSET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 'element'), - ['LSET', 'key', '0', 'element'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LSET.transformArguments('key', 0, 'element'), + ['LSET', 'key', '0', 'element'] + ); + }); - testUtils.testWithClient('client.lSet', async client => { - await client.lPush('key', 'element'); - assert.equal( - await client.lSet('key', 0, 'element'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lSet', async cluster => { - await cluster.lPush('key', 'element'); - assert.equal( - await cluster.lSet('key', 0, 'element'), - 'OK' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lSet', async client => { + await client.lPush('key', 'element'); + assert.equal( + await client.lSet('key', 0, 'element'), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LSET.ts b/packages/client/lib/commands/LSET.ts index 33c7b4cc060..edb86baae0f 100644 --- a/packages/client/lib/commands/LSET.ts +++ b/packages/client/lib/commands/LSET.ts @@ -1,18 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, index: number, - element: RedisCommandArgument -): RedisCommandArguments { + element: RedisArgument + ) { return [ - 'LSET', - key, - index.toString(), - element + 'LSET', + key, + index.toString(), + element ]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/LTRIM.spec.ts b/packages/client/lib/commands/LTRIM.spec.ts index 5b6ac5d3660..3edc3669737 100644 --- a/packages/client/lib/commands/LTRIM.spec.ts +++ b/packages/client/lib/commands/LTRIM.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LTRIM'; +import LTRIM from './LTRIM'; describe('LTRIM', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, -1), - ['LTRIM', 'key', '0', '-1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + LTRIM.transformArguments('key', 0, -1), + ['LTRIM', 'key', '0', '-1'] + ); + }); - testUtils.testWithClient('client.lTrim', async client => { - assert.equal( - await client.lTrim('key', 0, -1), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.lTrim', async cluster => { - assert.equal( - await cluster.lTrim('key', 0, -1), - 'OK' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('lTrim', async client => { + assert.equal( + await client.lTrim('key', 0, -1), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/LTRIM.ts b/packages/client/lib/commands/LTRIM.ts index 668497cdde6..b80aa6264e9 100644 --- a/packages/client/lib/commands/LTRIM.ts +++ b/packages/client/lib/commands/LTRIM.ts @@ -1,18 +1,18 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, start: number, stop: number -): RedisCommandArguments { + ) { return [ - 'LTRIM', - key, - start.toString(), - stop.toString() + 'LTRIM', + key, + start.toString(), + stop.toString() ]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts b/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts index ad97047606c..e1d718d7aab 100644 --- a/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts +++ b/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MEMORY_DOCTOR'; +import MEMORY_DOCTOR from './MEMORY_DOCTOR'; describe('MEMORY DOCTOR', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['MEMORY', 'DOCTOR'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MEMORY_DOCTOR.transformArguments(), + ['MEMORY', 'DOCTOR'] + ); + }); - testUtils.testWithClient('client.memoryDoctor', async client => { - assert.equal( - typeof (await client.memoryDoctor()), - 'string' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.memoryDoctor', async client => { + assert.equal( + typeof (await client.memoryDoctor()), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/MEMORY_DOCTOR.ts b/packages/client/lib/commands/MEMORY_DOCTOR.ts index 95a37246ffa..6f8eb29ef2b 100644 --- a/packages/client/lib/commands/MEMORY_DOCTOR.ts +++ b/packages/client/lib/commands/MEMORY_DOCTOR.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['MEMORY', 'DOCTOR']; -} +import { BlobStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['MEMORY', 'DOCTOR']; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts b/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts index ce866f1e116..8484fcbf0b7 100644 --- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts +++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MEMORY_MALLOC-STATS'; +import MEMORY_MALLOC_STATS from './MEMORY_MALLOC-STATS'; describe('MEMORY MALLOC-STATS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['MEMORY', 'MALLOC-STATS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MEMORY_MALLOC_STATS.transformArguments(), + ['MEMORY', 'MALLOC-STATS'] + ); + }); - testUtils.testWithClient('client.memoryMallocStats', async client => { - assert.equal( - typeof (await client.memoryDoctor()), - 'string' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.memoryMallocStats', async client => { + assert.equal( + typeof (await client.memoryMallocStats()), + 'string' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts index 3977e3a1de4..31b01a49b5c 100644 --- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts +++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { +import { BlobStringReply, Command } from '../RESP/types'; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['MEMORY', 'MALLOC-STATS']; -} + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; -export declare function transformReply(): string; diff --git a/packages/client/lib/commands/MEMORY_PURGE.spec.ts b/packages/client/lib/commands/MEMORY_PURGE.spec.ts index 5d34331feb6..41b01f87c4b 100644 --- a/packages/client/lib/commands/MEMORY_PURGE.spec.ts +++ b/packages/client/lib/commands/MEMORY_PURGE.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MEMORY_PURGE'; +import MEMORY_PURGE from './MEMORY_PURGE'; describe('MEMORY PURGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['MEMORY', 'PURGE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MEMORY_PURGE.transformArguments(), + ['MEMORY', 'PURGE'] + ); + }); - testUtils.testWithClient('client.memoryPurge', async client => { - assert.equal( - await client.memoryPurge(), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.memoryPurge', async client => { + assert.equal( + await client.memoryPurge(), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/MEMORY_PURGE.ts b/packages/client/lib/commands/MEMORY_PURGE.ts index cfa38179273..b4c48d40102 100644 --- a/packages/client/lib/commands/MEMORY_PURGE.ts +++ b/packages/client/lib/commands/MEMORY_PURGE.ts @@ -1,5 +1,11 @@ -export function transformArguments(): Array { +import { SimpleStringReply, Command } from '../RESP/types'; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments() { return ['MEMORY', 'PURGE']; -} + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; -export declare function transformReply(): string; diff --git a/packages/client/lib/commands/MEMORY_STATS.spec.ts b/packages/client/lib/commands/MEMORY_STATS.spec.ts index 12aa21181e6..6d5f5b8690b 100644 --- a/packages/client/lib/commands/MEMORY_STATS.spec.ts +++ b/packages/client/lib/commands/MEMORY_STATS.spec.ts @@ -1,108 +1,46 @@ -import { strict as assert } from 'assert'; -import { transformArguments, transformReply } from './MEMORY_STATS'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MEMORY_STATS from './MEMORY_STATS'; describe('MEMORY STATS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['MEMORY', 'STATS'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MEMORY_STATS.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 - } - } - } - ); - }); + testUtils.testWithClient('client.memoryStats', async client => { + const memoryStats = await client.memoryStats(); + assert.equal(typeof memoryStats['peak.allocated'], 'number'); + assert.equal(typeof memoryStats['total.allocated'], 'number'); + assert.equal(typeof memoryStats['startup.allocated'], 'number'); + assert.equal(typeof memoryStats['replication.backlog'], 'number'); + assert.equal(typeof memoryStats['clients.slaves'], 'number'); + assert.equal(typeof memoryStats['clients.normal'], 'number'); + assert.equal(typeof memoryStats['aof.buffer'], 'number'); + assert.equal(typeof memoryStats['lua.caches'], 'number'); + assert.equal(typeof memoryStats['overhead.total'], 'number'); + assert.equal(typeof memoryStats['keys.count'], 'number'); + assert.equal(typeof memoryStats['keys.bytes-per-key'], 'number'); + assert.equal(typeof memoryStats['dataset.bytes'], 'number'); + assert.equal(typeof memoryStats['dataset.percentage'], 'number'); + assert.equal(typeof memoryStats['peak.percentage'], 'number'); + assert.equal(typeof memoryStats['allocator.allocated'], 'number'); + assert.equal(typeof memoryStats['allocator.active'], 'number'); + assert.equal(typeof memoryStats['allocator.resident'], 'number'); + assert.equal(typeof memoryStats['allocator-fragmentation.ratio'], 'number', 'allocator-fragmentation.ratio'); + assert.equal(typeof memoryStats['allocator-fragmentation.bytes'], 'number'); + assert.equal(typeof memoryStats['allocator-rss.ratio'], 'number', 'allocator-rss.ratio'); + assert.equal(typeof memoryStats['allocator-rss.bytes'], 'number'); + assert.equal(typeof memoryStats['rss-overhead.ratio'], 'number', 'rss-overhead.ratio'); + assert.equal(typeof memoryStats['rss-overhead.bytes'], 'number'); + assert.equal(typeof memoryStats['fragmentation'], 'number', 'fragmentation'); + assert.equal(typeof memoryStats['fragmentation.bytes'], 'number'); + + if (testUtils.isVersionGreaterThan([7])) { + assert.equal(typeof memoryStats['cluster.links'], 'number'); + assert.equal(typeof memoryStats['functions.caches'], 'number'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/MEMORY_STATS.ts b/packages/client/lib/commands/MEMORY_STATS.ts index 8ae83d2239a..f38a0e5f29b 100644 --- a/packages/client/lib/commands/MEMORY_STATS.ts +++ b/packages/client/lib/commands/MEMORY_STATS.ts @@ -1,93 +1,68 @@ -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' - }; +import { TuplesToMapReply, BlobStringReply, NumberReply, DoubleReply, ArrayReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; -export function transformReply(rawReply: Array>): MemoryStatsReply { - const reply: any = { - db: {} - }; +export type MemoryStatsReply = TuplesToMapReply<[ + [BlobStringReply<'peak.allocated'>, NumberReply], + [BlobStringReply<'total.allocated'>, NumberReply], + [BlobStringReply<'startup.allocated'>, NumberReply], + [BlobStringReply<'replication.backlog'>, NumberReply], + [BlobStringReply<'clients.slaves'>, NumberReply], + [BlobStringReply<'clients.normal'>, NumberReply], + /** added in 7.0 */ + [BlobStringReply<'cluster.links'>, NumberReply], + [BlobStringReply<'aof.buffer'>, NumberReply], + [BlobStringReply<'lua.caches'>, NumberReply], + /** added in 7.0 */ + [BlobStringReply<'functions.caches'>, NumberReply], + // FIXME: 'db.0', and perhaps others' is here and is a map that should be handled? + [BlobStringReply<'overhead.total'>, NumberReply], + [BlobStringReply<'keys.count'>, NumberReply], + [BlobStringReply<'keys.bytes-per-key'>, NumberReply], + [BlobStringReply<'dataset.bytes'>, NumberReply], + [BlobStringReply<'dataset.percentage'>, DoubleReply], + [BlobStringReply<'peak.percentage'>, DoubleReply], + [BlobStringReply<'allocator.allocated'>, NumberReply], + [BlobStringReply<'allocator.active'>, NumberReply], + [BlobStringReply<'allocator.resident'>, NumberReply], + [BlobStringReply<'allocator-fragmentation.ratio'>, DoubleReply], + [BlobStringReply<'allocator-fragmentation.bytes'>, NumberReply], + [BlobStringReply<'allocator-rss.ratio'>, DoubleReply], + [BlobStringReply<'allocator-rss.bytes'>, NumberReply], + [BlobStringReply<'rss-overhead.ratio'>, DoubleReply], + [BlobStringReply<'rss-overhead.bytes'>, NumberReply], + [BlobStringReply<'fragmentation'>, DoubleReply], + [BlobStringReply<'fragmentation.bytes'>, NumberReply] +]>; - 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]; - } +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['MEMORY', 'STATS']; + }, + transformReply: { + 2: (rawReply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { + const reply: any = {}; - reply.db[key.substring(3)] = db; - continue; + let i = 0; + while (i < rawReply.length) { + switch(rawReply[i].toString()) { + case 'dataset.percentage': + case 'peak.percentage': + case 'allocator-fragmentation.ratio': + case 'allocator-rss.ratio': + case 'rss-overhead.ratio': + case 'fragmentation': + reply[rawReply[i++] as any] = transformDoubleReply[2](rawReply[i++] as unknown as BlobStringReply, preserve, typeMapping); + break; + default: + reply[rawReply[i++] as any] = rawReply[i++]; } + + } - reply[FIELDS_MAPPING[key as keyof typeof FIELDS_MAPPING]] = Number(rawReply[i + 1]); - } - - return reply as MemoryStatsReply; -} + return reply as MemoryStatsReply; + }, + 3: undefined as unknown as () => MemoryStatsReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/MEMORY_USAGE.spec.ts b/packages/client/lib/commands/MEMORY_USAGE.spec.ts index fe5ff404d93..250a6884259 100644 --- a/packages/client/lib/commands/MEMORY_USAGE.spec.ts +++ b/packages/client/lib/commands/MEMORY_USAGE.spec.ts @@ -1,30 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MEMORY_USAGE'; +import MEMORY_USAGE from './MEMORY_USAGE'; describe('MEMORY USAGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key'), - ['MEMORY', 'USAGE', 'key'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + MEMORY_USAGE.transformArguments('key'), + ['MEMORY', 'USAGE', 'key'] + ); + }); - it('with SAMPLES', () => { - assert.deepEqual( - transformArguments('key', { - SAMPLES: 1 - }), - ['MEMORY', 'USAGE', 'key', 'SAMPLES', '1'] - ); - }); + it('with SAMPLES', () => { + assert.deepEqual( + MEMORY_USAGE.transformArguments('key', { + SAMPLES: 1 + }), + ['MEMORY', 'USAGE', 'key', 'SAMPLES', '1'] + ); }); + }); - testUtils.testWithClient('client.memoryUsage', async client => { - assert.equal( - await client.memoryUsage('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.memoryUsage', async client => { + assert.equal( + await client.memoryUsage('key'), + null + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/MEMORY_USAGE.ts b/packages/client/lib/commands/MEMORY_USAGE.ts index 959cdb0a0c4..78b079c2c13 100644 --- a/packages/client/lib/commands/MEMORY_USAGE.ts +++ b/packages/client/lib/commands/MEMORY_USAGE.ts @@ -1,19 +1,20 @@ -export const FIRST_KEY_INDEX = 1; +import { NumberReply, NullReply, Command, RedisArgument } from '../RESP/types'; -export const IS_READ_ONLY = true; - -interface MemoryUsageOptions { - SAMPLES?: number; +export interface MemoryUsageOptions { + SAMPLES?: number; } -export function transformArguments(key: string, options?: MemoryUsageOptions): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, options?: MemoryUsageOptions) { const args = ['MEMORY', 'USAGE', key]; if (options?.SAMPLES) { - args.push('SAMPLES', options.SAMPLES.toString()); + args.push('SAMPLES', options.SAMPLES.toString()); } - + return args; -} - -export declare function transformReply(): number | null; + }, + transformReply: undefined as unknown as () => NumberReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/MGET.spec.ts b/packages/client/lib/commands/MGET.spec.ts index 9ff47895f4e..296702a1a03 100644 --- a/packages/client/lib/commands/MGET.spec.ts +++ b/packages/client/lib/commands/MGET.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MGET'; +import MGET from './MGET'; describe('MGET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['MGET', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MGET.transformArguments(['1', '2']), + ['MGET', '1', '2'] + ); + }); - testUtils.testWithClient('client.mGet', async client => { - assert.deepEqual( - await client.mGet(['key']), - [null] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.mGet', async cluster => { - assert.deepEqual( - await cluster.mGet(['key']), - [null] - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('mGet', async client => { + assert.deepEqual( + await client.mGet(['key']), + [null] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/MGET.ts b/packages/client/lib/commands/MGET.ts index 6635a2ca20c..0f0f9e52ffb 100644 --- a/packages/client/lib/commands/MGET.ts +++ b/packages/client/lib/commands/MGET.ts @@ -1,13 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - keys: Array -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(keys: Array) { return ['MGET', ...keys]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => Array +} as const satisfies Command; diff --git a/packages/client/lib/commands/MIGRATE.spec.ts b/packages/client/lib/commands/MIGRATE.spec.ts index ca7ceb48b39..880d59a09c7 100644 --- a/packages/client/lib/commands/MIGRATE.spec.ts +++ b/packages/client/lib/commands/MIGRATE.spec.ts @@ -1,76 +1,76 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './MIGRATE'; +import { strict as assert } from 'node:assert'; +import MIGRATE 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'] - ); - }); + describe('transformArguments', () => { + it('single key', () => { + assert.deepEqual( + MIGRATE.transformArguments('127.0.0.1', 6379, 'key', 0, 10), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10'] + ); + }); + + it('multiple keys', () => { + assert.deepEqual( + MIGRATE.transformArguments('127.0.0.1', 6379, ['1', '2'], 0, 10), + ['MIGRATE', '127.0.0.1', '6379', '', '0', '10', 'KEYS', '1', '2'] + ); + }); - 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( + MIGRATE.transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + COPY: true + }), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'COPY'] + ); + }); - 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( + MIGRATE.transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + REPLACE: true + }), + ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'REPLACE'] + ); + }); - 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'] - ); - }); + describe('with AUTH', () => { + it('password only', () => { + assert.deepEqual( + MIGRATE.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('username & password', () => { + assert.deepEqual( + MIGRATE.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'] - ); - }); + it('with COPY, REPLACE, AUTH', () => { + assert.deepEqual( + MIGRATE.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/packages/client/lib/commands/MIGRATE.ts b/packages/client/lib/commands/MIGRATE.ts index aaff3164081..4821f93fcfb 100644 --- a/packages/client/lib/commands/MIGRATE.ts +++ b/packages/client/lib/commands/MIGRATE.ts @@ -1,65 +1,67 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; import { AuthOptions } from './AUTH'; -interface MigrateOptions { - COPY?: true; - REPLACE?: true; - AUTH?: AuthOptions; +export interface MigrateOptions { + COPY?: true; + REPLACE?: true; + AUTH?: AuthOptions; } -export function transformArguments( - host: RedisCommandArgument, +export default { + IS_READ_ONLY: false, + transformArguments( + host: RedisArgument, port: number, - key: RedisCommandArgument | Array, + key: RedisArgument | Array, destinationDb: number, timeout: number, options?: MigrateOptions -): RedisCommandArguments { + ) { const args = ['MIGRATE', host, port.toString()], - isKeyArray = Array.isArray(key); - + isKeyArray = Array.isArray(key); + if (isKeyArray) { - args.push(''); + args.push(''); } else { - args.push(key); + args.push(key); } - + args.push( - destinationDb.toString(), - timeout.toString() + destinationDb.toString(), + timeout.toString() ); - + if (options?.COPY) { - args.push('COPY'); + args.push('COPY'); } - + if (options?.REPLACE) { - args.push('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 (isKeyArray) { + if (options.AUTH.username) { args.push( - 'KEYS', - ...key + 'AUTH2', + options.AUTH.username, + options.AUTH.password ); + } else { + args.push( + 'AUTH', + options.AUTH.password + ); + } } - + + if (isKeyArray) { + args.push( + 'KEYS', + ...key + ); + } + return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/MODULE_LIST.spec.ts b/packages/client/lib/commands/MODULE_LIST.spec.ts index eeeb774ebff..9c1d3874381 100644 --- a/packages/client/lib/commands/MODULE_LIST.spec.ts +++ b/packages/client/lib/commands/MODULE_LIST.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './MODULE_LIST'; +import { strict as assert } from 'node:assert'; +import MODULE_LIST from './MODULE_LIST'; describe('MODULE LIST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['MODULE', 'LIST'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MODULE_LIST.transformArguments(), + ['MODULE', 'LIST'] + ); + }); }); diff --git a/packages/client/lib/commands/MODULE_LIST.ts b/packages/client/lib/commands/MODULE_LIST.ts index d75b2428308..5ddd4e91ff6 100644 --- a/packages/client/lib/commands/MODULE_LIST.ts +++ b/packages/client/lib/commands/MODULE_LIST.ts @@ -1,5 +1,26 @@ -export function transformArguments(): Array { - return ['MODULE', 'LIST']; -} +import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; + +export type ModuleListReply = ArrayReply, BlobStringReply], + [BlobStringReply<'ver'>, NumberReply], +]>>; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['MODULE', 'LIST']; + }, + transformReply: { + 2: (reply: UnwrapReply>) => { + return reply.map(module => { + const unwrapped = module as unknown as UnwrapReply; + return { + name: unwrapped[1], + ver: unwrapped[3] + }; + }); + }, + 3: undefined as unknown as () => ModuleListReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/MODULE_LOAD.spec.ts b/packages/client/lib/commands/MODULE_LOAD.spec.ts index 5a99a232ca4..3b7b9dd00a3 100644 --- a/packages/client/lib/commands/MODULE_LOAD.spec.ts +++ b/packages/client/lib/commands/MODULE_LOAD.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './MODULE_LOAD'; +import { strict as assert } from 'node:assert'; +import MODULE_LOAD from './MODULE_LOAD'; describe('MODULE LOAD', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('path'), - ['MODULE', 'LOAD', 'path'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + MODULE_LOAD.transformArguments('path'), + ['MODULE', 'LOAD', 'path'] + ); + }); - it('with module args', () => { - assert.deepEqual( - transformArguments('path', ['1', '2']), - ['MODULE', 'LOAD', 'path', '1', '2'] - ); - }); + it('with module args', () => { + assert.deepEqual( + MODULE_LOAD.transformArguments('path', ['1', '2']), + ['MODULE', 'LOAD', 'path', '1', '2'] + ); }); + }); }); diff --git a/packages/client/lib/commands/MODULE_LOAD.ts b/packages/client/lib/commands/MODULE_LOAD.ts index b44b4b57ce6..82c5b81a905 100644 --- a/packages/client/lib/commands/MODULE_LOAD.ts +++ b/packages/client/lib/commands/MODULE_LOAD.ts @@ -1,11 +1,16 @@ -export function transformArguments(path: string, moduleArgs?: Array): Array { +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(path: RedisArgument, moduleArguments?: Array) { const args = ['MODULE', 'LOAD', path]; - if (moduleArgs) { - args.push(...moduleArgs); + if (moduleArguments) { + return args.concat(moduleArguments); } - + return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/MODULE_UNLOAD.spec.ts b/packages/client/lib/commands/MODULE_UNLOAD.spec.ts index d8af96c54f1..e33fb3a5f4f 100644 --- a/packages/client/lib/commands/MODULE_UNLOAD.spec.ts +++ b/packages/client/lib/commands/MODULE_UNLOAD.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './MODULE_UNLOAD'; +import { strict as assert } from 'node:assert'; +import MODULE_UNLOAD from './MODULE_UNLOAD'; describe('MODULE UNLOAD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('name'), - ['MODULE', 'UNLOAD', 'name'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MODULE_UNLOAD.transformArguments('name'), + ['MODULE', 'UNLOAD', 'name'] + ); + }); }); diff --git a/packages/client/lib/commands/MODULE_UNLOAD.ts b/packages/client/lib/commands/MODULE_UNLOAD.ts index d5927778fe6..3a2bc05c0e7 100644 --- a/packages/client/lib/commands/MODULE_UNLOAD.ts +++ b/packages/client/lib/commands/MODULE_UNLOAD.ts @@ -1,5 +1,10 @@ -export function transformArguments(name: string): Array { - return ['MODULE', 'UNLOAD', name]; -} +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(name: RedisArgument) { + return ['MODULE', 'UNLOAD', name]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/MOVE.spec.ts b/packages/client/lib/commands/MOVE.spec.ts index f7fdc481cbf..e767568e72b 100644 --- a/packages/client/lib/commands/MOVE.spec.ts +++ b/packages/client/lib/commands/MOVE.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MOVE'; +import MOVE from './MOVE'; describe('MOVE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['MOVE', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + MOVE.transformArguments('key', 1), + ['MOVE', 'key', '1'] + ); + }); - testUtils.testWithClient('client.move', async client => { - assert.equal( - await client.move('key', 1), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.move', async client => { + assert.equal( + await client.move('key', 1), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/MOVE.ts b/packages/client/lib/commands/MOVE.ts index 17cc6742c5f..60aac4b1457 100644 --- a/packages/client/lib/commands/MOVE.ts +++ b/packages/client/lib/commands/MOVE.ts @@ -1,7 +1,9 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export function transformArguments(key: string, db: number): Array { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, db: number) { return ['MOVE', key, db.toString()]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/MSET.spec.ts b/packages/client/lib/commands/MSET.spec.ts index 0568f38487e..5435e283108 100644 --- a/packages/client/lib/commands/MSET.spec.ts +++ b/packages/client/lib/commands/MSET.spec.ts @@ -1,42 +1,38 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MSET'; +import MSET 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'] - ); - }); + describe('transformArguments', () => { + it("['key1', 'value1', 'key2', 'value2']", () => { + assert.deepEqual( + MSET.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( + MSET.transformArguments([['key1', 'value1'], ['key2', 'value2']]), + ['MSET', 'key1', 'value1', 'key2', 'value2'] + ); }); - testUtils.testWithClient('client.mSet', async client => { - assert.equal( - await client.mSet(['key1', 'value1', 'key2', 'value2']), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + it("{key1: 'value1'. key2: 'value2'}", () => { + assert.deepEqual( + MSET.transformArguments({ key1: 'value1', key2: 'value2' }), + ['MSET', 'key1', 'value1', 'key2', 'value2'] + ); + }); + }); - testUtils.testWithCluster('cluster.mSet', async cluster => { - assert.equal( - await cluster.mSet(['{key}1', 'value1', '{key}2', 'value2']), - 'OK' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('mSet', async client => { + assert.equal( + await client.mSet(['{tag}key1', 'value1', '{tag}key2', 'value2']), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/MSET.ts b/packages/client/lib/commands/MSET.ts index bd7111659d1..136fde3e7f7 100644 --- a/packages/client/lib/commands/MSET.ts +++ b/packages/client/lib/commands/MSET.ts @@ -1,24 +1,27 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export type MSetArguments = - Array<[RedisCommandArgument, RedisCommandArgument]> | - Array | - Record; + Array<[RedisArgument, RedisArgument]> | + Array | + Record; -export function transformArguments(toSet: MSetArguments): RedisCommandArguments { - const args: RedisCommandArguments = ['MSET']; +export function mSetArguments(command: string, toSet: MSetArguments) { + const args: Array = [command]; - if (Array.isArray(toSet)) { - args.push(...toSet.flat()); - } else { - for (const key of Object.keys(toSet)) { - args.push(key, toSet[key]); - } + if (Array.isArray(toSet)) { + args.push(...toSet.flat()); + } else { + for (const tuple of Object.entries(toSet)) { + args.push(...tuple); } + } - return args; + return args; } -export declare function transformReply(): RedisCommandArgument; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments: mSetArguments.bind(undefined, 'MSET'), + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/MSETNX.spec.ts b/packages/client/lib/commands/MSETNX.spec.ts index 854a9affd8a..1583d04a94e 100644 --- a/packages/client/lib/commands/MSETNX.spec.ts +++ b/packages/client/lib/commands/MSETNX.spec.ts @@ -1,42 +1,38 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MSETNX'; +import MSETNX 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'] - ); - }); + describe('transformArguments', () => { + it("['key1', 'value1', 'key2', 'value2']", () => { + assert.deepEqual( + MSETNX.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( + MSETNX.transformArguments([['key1', 'value1'], ['key2', 'value2']]), + ['MSETNX', 'key1', 'value1', 'key2', 'value2'] + ); }); - testUtils.testWithClient('client.mSetNX', async client => { - assert.equal( - await client.mSetNX(['key1', 'value1', 'key2', 'value2']), - true - ); - }, GLOBAL.SERVERS.OPEN); + it("{key1: 'value1'. key2: 'value2'}", () => { + assert.deepEqual( + MSETNX.transformArguments({ key1: 'value1', key2: 'value2' }), + ['MSETNX', 'key1', 'value1', 'key2', 'value2'] + ); + }); + }); - testUtils.testWithCluster('cluster.mSetNX', async cluster => { - assert.equal( - await cluster.mSetNX(['{key}1', 'value1', '{key}2', 'value2']), - true - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('mSetNX', async client => { + assert.equal( + await client.mSetNX(['{key}1', 'value1', '{key}2', 'value2']), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/MSETNX.ts b/packages/client/lib/commands/MSETNX.ts index 0ef33936114..4867b3ea092 100644 --- a/packages/client/lib/commands/MSETNX.ts +++ b/packages/client/lib/commands/MSETNX.ts @@ -1,20 +1,9 @@ -import { RedisCommandArguments } from '.'; -import { MSetArguments } from './MSET'; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(toSet: MSetArguments): RedisCommandArguments { - const args: RedisCommandArguments = ['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 { transformBooleanReply as transformReply } from './generic-transformers'; +import { SimpleStringReply, Command } from '../RESP/types'; +import { mSetArguments } from './MSET'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments: mSetArguments.bind(undefined, 'MSETNX'), + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/OBJECT_ENCODING.spec.ts b/packages/client/lib/commands/OBJECT_ENCODING.spec.ts index 6f42969d547..48146f12924 100644 --- a/packages/client/lib/commands/OBJECT_ENCODING.spec.ts +++ b/packages/client/lib/commands/OBJECT_ENCODING.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './OBJECT_ENCODING'; +import OBJECT_ENCODING from './OBJECT_ENCODING'; describe('OBJECT ENCODING', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['OBJECT', 'ENCODING', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + OBJECT_ENCODING.transformArguments('key'), + ['OBJECT', 'ENCODING', 'key'] + ); + }); - testUtils.testWithClient('client.objectEncoding', async client => { - assert.equal( - await client.objectEncoding('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('objectEncoding', async client => { + assert.equal( + await client.objectEncoding('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/OBJECT_ENCODING.ts b/packages/client/lib/commands/OBJECT_ENCODING.ts index ac219ae89ed..e74c3f99ebd 100644 --- a/packages/client/lib/commands/OBJECT_ENCODING.ts +++ b/packages/client/lib/commands/OBJECT_ENCODING.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['OBJECT', 'ENCODING', key]; -} - -export declare function transformReply(): string | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/OBJECT_FREQ.spec.ts b/packages/client/lib/commands/OBJECT_FREQ.spec.ts index 6d2513cf18c..cbad72c308d 100644 --- a/packages/client/lib/commands/OBJECT_FREQ.spec.ts +++ b/packages/client/lib/commands/OBJECT_FREQ.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './OBJECT_FREQ'; +import OBJECT_FREQ from './OBJECT_FREQ'; describe('OBJECT FREQ', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['OBJECT', 'FREQ', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + OBJECT_FREQ.transformArguments('key'), + ['OBJECT', 'FREQ', 'key'] + ); + }); - testUtils.testWithClient('client.objectFreq', async client => { - assert.equal( - await client.objectFreq('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('client.objectFreq', async client => { + assert.equal( + await client.objectFreq('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/OBJECT_FREQ.ts b/packages/client/lib/commands/OBJECT_FREQ.ts index 071d16f2748..b6757c6c57b 100644 --- a/packages/client/lib/commands/OBJECT_FREQ.ts +++ b/packages/client/lib/commands/OBJECT_FREQ.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['OBJECT', 'FREQ', key]; -} - -export declare function transformReply(): number | null; + }, + transformReply: undefined as unknown as () => NumberReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts b/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts index 61529e1366b..51866427c81 100644 --- a/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts +++ b/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './OBJECT_IDLETIME'; +import OBJECT_IDLETIME from './OBJECT_IDLETIME'; describe('OBJECT IDLETIME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['OBJECT', 'IDLETIME', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + OBJECT_IDLETIME.transformArguments('key'), + ['OBJECT', 'IDLETIME', 'key'] + ); + }); - testUtils.testWithClient('client.objectIdleTime', async client => { - assert.equal( - await client.objectIdleTime('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('client.objectIdleTime', async client => { + assert.equal( + await client.objectIdleTime('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/OBJECT_IDLETIME.ts b/packages/client/lib/commands/OBJECT_IDLETIME.ts index 38847d6f4cf..f0fef24e2d8 100644 --- a/packages/client/lib/commands/OBJECT_IDLETIME.ts +++ b/packages/client/lib/commands/OBJECT_IDLETIME.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['OBJECT', 'IDLETIME', key]; -} - -export declare function transformReply(): number | null; + }, + transformReply: undefined as unknown as () => NumberReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts b/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts index 199dca3fe82..f4309786eb1 100644 --- a/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts +++ b/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './OBJECT_REFCOUNT'; +import OBJECT_REFCOUNT from './OBJECT_REFCOUNT'; describe('OBJECT REFCOUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['OBJECT', 'REFCOUNT', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + OBJECT_REFCOUNT.transformArguments('key'), + ['OBJECT', 'REFCOUNT', 'key'] + ); + }); - testUtils.testWithClient('client.objectRefCount', async client => { - assert.equal( - await client.objectRefCount('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('client.objectRefCount', async client => { + assert.equal( + await client.objectRefCount('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/OBJECT_REFCOUNT.ts b/packages/client/lib/commands/OBJECT_REFCOUNT.ts index 9fd259b5b90..c8381a76bf7 100644 --- a/packages/client/lib/commands/OBJECT_REFCOUNT.ts +++ b/packages/client/lib/commands/OBJECT_REFCOUNT.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['OBJECT', 'REFCOUNT', key]; -} - -export declare function transformReply(): number | null; + }, + transformReply: undefined as unknown as () => NumberReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PERSIST.spec.ts b/packages/client/lib/commands/PERSIST.spec.ts index 4e53bd85a6c..d778a1c0a5f 100644 --- a/packages/client/lib/commands/PERSIST.spec.ts +++ b/packages/client/lib/commands/PERSIST.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PERSIST'; +import PERSIST from './PERSIST'; describe('PERSIST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['PERSIST', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + PERSIST.transformArguments('key'), + ['PERSIST', 'key'] + ); + }); - testUtils.testWithClient('client.persist', async client => { - assert.equal( - await client.persist('key'), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('persist', async client => { + assert.equal( + await client.persist('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PERSIST.ts b/packages/client/lib/commands/PERSIST.ts index d7c9f8623e4..64637fabc14 100644 --- a/packages/client/lib/commands/PERSIST.ts +++ b/packages/client/lib/commands/PERSIST.ts @@ -1,9 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument) { return ['PERSIST', key]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PEXPIRE.spec.ts b/packages/client/lib/commands/PEXPIRE.spec.ts index 03bde656103..61bfe69e9a1 100644 --- a/packages/client/lib/commands/PEXPIRE.spec.ts +++ b/packages/client/lib/commands/PEXPIRE.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PEXPIRE'; +import PEXPIRE from './PEXPIRE'; describe('PEXPIRE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 1), - ['PEXPIRE', 'key', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + PEXPIRE.transformArguments('key', 1), + ['PEXPIRE', 'key', '1'] + ); + }); - it('with set option', () => { - assert.deepEqual( - transformArguments('key', 1, 'GT'), - ['PEXPIRE', 'key', '1', 'GT'] - ); - }); + it('with set option', () => { + assert.deepEqual( + PEXPIRE.transformArguments('key', 1, 'GT'), + ['PEXPIRE', 'key', '1', 'GT'] + ); }); + }); - testUtils.testWithClient('client.pExpire', async client => { - assert.equal( - await client.pExpire('key', 1), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('pExpire', async client => { + assert.equal( + await client.pExpire('key', 1), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PEXPIRE.ts b/packages/client/lib/commands/PEXPIRE.ts index cbb5666a514..4d64281e922 100644 --- a/packages/client/lib/commands/PEXPIRE.ts +++ b/packages/client/lib/commands/PEXPIRE.ts @@ -1,19 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - milliseconds: number, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + ms: number, mode?: 'NX' | 'XX' | 'GT' | 'LT' -): RedisCommandArguments { - const args = ['PEXPIRE', key, milliseconds.toString()]; + ) { + const args = ['PEXPIRE', key, ms.toString()]; if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PEXPIREAT.spec.ts b/packages/client/lib/commands/PEXPIREAT.spec.ts index fec03c8fb75..117c5b512e7 100644 --- a/packages/client/lib/commands/PEXPIREAT.spec.ts +++ b/packages/client/lib/commands/PEXPIREAT.spec.ts @@ -1,36 +1,39 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PEXPIREAT'; +import PEXPIREAT from './PEXPIREAT'; describe('PEXPIREAT', () => { - describe('transformArguments', () => { - it('number', () => { - assert.deepEqual( - transformArguments('key', 1), - ['PEXPIREAT', 'key', '1'] - ); - }); + describe('transformArguments', () => { + it('number', () => { + assert.deepEqual( + PEXPIREAT.transformArguments('key', 1), + ['PEXPIREAT', 'key', '1'] + ); + }); - it('date', () => { - const d = new Date(); - assert.deepEqual( - transformArguments('key', d), - ['PEXPIREAT', 'key', d.getTime().toString()] - ); - }); + it('date', () => { + const d = new Date(); + assert.deepEqual( + PEXPIREAT.transformArguments('key', d), + ['PEXPIREAT', 'key', d.getTime().toString()] + ); + }); - it('with set option', () => { - assert.deepEqual( - transformArguments('key', 1, 'XX'), - ['PEXPIREAT', 'key', '1', 'XX'] - ); - }); + it('with set option', () => { + assert.deepEqual( + PEXPIREAT.transformArguments('key', 1, 'XX'), + ['PEXPIREAT', 'key', '1', 'XX'] + ); }); + }); - testUtils.testWithClient('client.pExpireAt', async client => { - assert.equal( - await client.pExpireAt('key', 1), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('pExpireAt', async client => { + assert.equal( + await client.pExpireAt('key', 1), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PEXPIREAT.ts b/packages/client/lib/commands/PEXPIREAT.ts index da912ec4fcb..cbae589fba6 100644 --- a/packages/client/lib/commands/PEXPIREAT.ts +++ b/packages/client/lib/commands/PEXPIREAT.ts @@ -1,24 +1,21 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformPXAT } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - millisecondsTimestamp: number | Date, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + msTimestamp: number | Date, mode?: 'NX' | 'XX' | 'GT' | 'LT' -): RedisCommandArguments { - const args = [ - 'PEXPIREAT', - key, - transformPXAT(millisecondsTimestamp) - ]; + ) { + const args = ['PEXPIREAT', key, transformPXAT(msTimestamp)]; if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PEXPIRETIME.spec.ts b/packages/client/lib/commands/PEXPIRETIME.spec.ts index a2fd7f03f82..25a8293ec59 100644 --- a/packages/client/lib/commands/PEXPIRETIME.spec.ts +++ b/packages/client/lib/commands/PEXPIRETIME.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PEXPIRETIME'; +import PEXPIRETIME from './PEXPIRETIME'; describe('PEXPIRETIME', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['PEXPIRETIME', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + PEXPIRETIME.transformArguments('key'), + ['PEXPIRETIME', 'key'] + ); + }); - testUtils.testWithClient('client.pExpireTime', async client => { - assert.equal( - await client.pExpireTime('key'), - -2 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('pExpireTime', async client => { + assert.equal( + await client.pExpireTime('key'), + -2 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PEXPIRETIME.ts b/packages/client/lib/commands/PEXPIRETIME.ts index 4c1acba8f04..e228351fa0e 100644 --- a/packages/client/lib/commands/PEXPIRETIME.ts +++ b/packages/client/lib/commands/PEXPIRETIME.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['PEXPIRETIME', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PFADD.spec.ts b/packages/client/lib/commands/PFADD.spec.ts index 8c0e752fd50..04ba44164df 100644 --- a/packages/client/lib/commands/PFADD.spec.ts +++ b/packages/client/lib/commands/PFADD.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PFADD'; +import PFADD from './PFADD'; describe('PFADD', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'element'), - ['PFADD', 'key', 'element'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + PFADD.transformArguments('key', 'element'), + ['PFADD', 'key', 'element'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['PFADD', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + PFADD.transformArguments('key', ['1', '2']), + ['PFADD', 'key', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.pfAdd', async client => { - assert.equal( - await client.pfAdd('key', '1'), - true - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('pfAdd', async client => { + assert.equal( + await client.pfAdd('key', '1'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PFADD.ts b/packages/client/lib/commands/PFADD.ts index 8c8985de890..d7f198fbe55 100644 --- a/packages/client/lib/commands/PFADD.ts +++ b/packages/client/lib/commands/PFADD.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, element?: RedisVariadicArgument) { + const args = ['PFADD', key]; + if (!element) return args; -export function transformArguments( - key: RedisCommandArgument, - element: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['PFADD', key], element); -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + return pushVariadicArguments(args, element); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PFCOUNT.spec.ts b/packages/client/lib/commands/PFCOUNT.spec.ts index a1ea06c4494..ebb54ea1d72 100644 --- a/packages/client/lib/commands/PFCOUNT.spec.ts +++ b/packages/client/lib/commands/PFCOUNT.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PFCOUNT'; +import PFCOUNT from './PFCOUNT'; describe('PFCOUNT', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['PFCOUNT', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + PFCOUNT.transformArguments('key'), + ['PFCOUNT', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['PFCOUNT', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + PFCOUNT.transformArguments(['1', '2']), + ['PFCOUNT', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.pfCount', async client => { - assert.equal( - await client.pfCount('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('pfCount', async client => { + assert.equal( + await client.pfCount('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PFCOUNT.ts b/packages/client/lib/commands/PFCOUNT.ts index a4cf2dbcb26..5b46eb00d92 100644 --- a/packages/client/lib/commands/PFCOUNT.ts +++ b/packages/client/lib/commands/PFCOUNT.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['PFCOUNT'], key); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisVariadicArgument) { + return pushVariadicArguments(['PFCOUNT'], key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PFMERGE.spec.ts b/packages/client/lib/commands/PFMERGE.spec.ts index 881fc5f5439..bb2444b3ef1 100644 --- a/packages/client/lib/commands/PFMERGE.spec.ts +++ b/packages/client/lib/commands/PFMERGE.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PFMERGE'; +import PFMERGE from './PFMERGE'; describe('PFMERGE', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('destination', 'source'), - ['PFMERGE', 'destination', 'source'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + PFMERGE.transformArguments('destination', 'source'), + ['PFMERGE', 'destination', 'source'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['PFMERGE', 'destination', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + PFMERGE.transformArguments('destination', ['1', '2']), + ['PFMERGE', 'destination', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.pfMerge', async client => { - assert.equal( - await client.pfMerge('destination', 'source'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('pfMerge', async client => { + assert.equal( + await client.pfMerge('{tag}destination', '{tag}source'), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PFMERGE.ts b/packages/client/lib/commands/PFMERGE.ts index e934062b3fe..eeeeb5173db 100644 --- a/packages/client/lib/commands/PFMERGE.ts +++ b/packages/client/lib/commands/PFMERGE.ts @@ -1,10 +1,16 @@ -import { RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + destination: RedisArgument, + source?: RedisVariadicArgument + ) { + const args = ['PFMERGE', destination]; + if (!source) return args; -export function transformArguments(destination: string, source: string | Array): RedisCommandArguments { - return pushVerdictArguments(['PFMERGE', destination], source); -} - -export declare function transformReply(): string; + return pushVariadicArguments(args, source); + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PING.spec.ts b/packages/client/lib/commands/PING.spec.ts index 06cbae43a13..0cd75a6a8de 100644 --- a/packages/client/lib/commands/PING.spec.ts +++ b/packages/client/lib/commands/PING.spec.ts @@ -1,37 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PING'; +import PING from './PING'; describe('PING', () => { - describe('transformArguments', () => { - it('default', () => { - assert.deepEqual( - transformArguments(), - ['PING'] - ); - }); - - it('with message', () => { - assert.deepEqual( - transformArguments('message'), - ['PING', 'message'] - ); - }); + describe('transformArguments', () => { + it('default', () => { + assert.deepEqual( + PING.transformArguments(), + ['PING'] + ); }); - describe('client.ping', () => { - testUtils.testWithClient('string', async client => { - assert.equal( - await client.ping(), - 'PONG' - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('buffer', async client => { - assert.deepEqual( - await client.ping(client.commandOptions({ returnBuffers: true })), - Buffer.from('PONG') - ); - }, GLOBAL.SERVERS.OPEN); + it('with message', () => { + assert.deepEqual( + PING.transformArguments('message'), + ['PING', 'message'] + ); }); + }); + + testUtils.testAll('ping', async client => { + assert.equal( + await client.ping(), + 'PONG' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PING.ts b/packages/client/lib/commands/PING.ts index 95fa006122d..7f6fd31047e 100644 --- a/packages/client/lib/commands/PING.ts +++ b/packages/client/lib/commands/PING.ts @@ -1,12 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(message?: RedisCommandArgument): RedisCommandArguments { - const args: RedisCommandArguments = ['PING']; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(message?: RedisArgument) { + const args: Array = ['PING']; if (message) { - args.push(message); + args.push(message); } return args; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply | BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PSETEX.spec.ts b/packages/client/lib/commands/PSETEX.spec.ts index f6262ed8709..fd7bcd2dff2 100644 --- a/packages/client/lib/commands/PSETEX.spec.ts +++ b/packages/client/lib/commands/PSETEX.spec.ts @@ -1,27 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PSETEX'; +import PSETEX from './PSETEX'; describe('PSETEX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1, 'value'), - ['PSETEX', 'key', '1', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + PSETEX.transformArguments('key', 1, 'value'), + ['PSETEX', 'key', '1', 'value'] + ); + }); - testUtils.testWithClient('client.pSetEx', async client => { - const a = await client.pSetEx('key', 1, 'value'); - assert.equal( - await client.pSetEx('key', 1, 'value'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.pSetEx', async cluster => { - assert.equal( - await cluster.pSetEx('key', 1, 'value'), - 'OK' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('pSetEx', async client => { + assert.equal( + await client.pSetEx('key', 1, 'value'), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PSETEX.ts b/packages/client/lib/commands/PSETEX.ts index f2739b6e274..4e345a1a1c9 100644 --- a/packages/client/lib/commands/PSETEX.ts +++ b/packages/client/lib/commands/PSETEX.ts @@ -1,18 +1,18 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - milliseconds: number, - value: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + ms: number, + value: RedisArgument + ) { return [ - 'PSETEX', - key, - milliseconds.toString(), - value + 'PSETEX', + key, + ms.toString(), + value ]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/PTTL.spec.ts b/packages/client/lib/commands/PTTL.spec.ts index e65421de590..65a9f5dd0ff 100644 --- a/packages/client/lib/commands/PTTL.spec.ts +++ b/packages/client/lib/commands/PTTL.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PTTL'; +import PTTL from './PTTL'; describe('PTTL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['PTTL', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + PTTL.transformArguments('key'), + ['PTTL', 'key'] + ); + }); - testUtils.testWithClient('client.pTTL', async client => { - assert.equal( - await client.pTTL('key'), - -2 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('pTTL', async client => { + assert.equal( + await client.pTTL('key'), + -2 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/PTTL.ts b/packages/client/lib/commands/PTTL.ts index a2975623f7a..35854337877 100644 --- a/packages/client/lib/commands/PTTL.ts +++ b/packages/client/lib/commands/PTTL.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['PTTL', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PUBLISH.spec.ts b/packages/client/lib/commands/PUBLISH.spec.ts index b2084e668ba..ec1f108e5ee 100644 --- a/packages/client/lib/commands/PUBLISH.spec.ts +++ b/packages/client/lib/commands/PUBLISH.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PUBLISH'; +import PUBLISH from './PUBLISH'; describe('PUBLISH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('channel', 'message'), - ['PUBLISH', 'channel', 'message'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + PUBLISH.transformArguments('channel', 'message'), + ['PUBLISH', 'channel', 'message'] + ); + }); - testUtils.testWithClient('client.publish', async client => { - assert.equal( - await client.publish('channel', 'message'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.publish', async client => { + assert.equal( + await client.publish('channel', 'message'), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/PUBLISH.ts b/packages/client/lib/commands/PUBLISH.ts index 7862a0936cb..e790ff16c4d 100644 --- a/packages/client/lib/commands/PUBLISH.ts +++ b/packages/client/lib/commands/PUBLISH.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments( - channel: RedisCommandArgument, - message: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + IS_FORWARD_COMMAND: true, + transformArguments(channel: RedisArgument, message: RedisArgument) { return ['PUBLISH', channel, message]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts b/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts index c427eab4850..2fe02523ed1 100644 --- a/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts +++ b/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts @@ -1,28 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PUBSUB_CHANNELS'; +import PUBSUB_CHANNELS from './PUBSUB_CHANNELS'; describe('PUBSUB CHANNELS', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['PUBSUB', 'CHANNELS'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + PUBSUB_CHANNELS.transformArguments(), + ['PUBSUB', 'CHANNELS'] + ); + }); - it('with pattern', () => { - assert.deepEqual( - transformArguments('patter*'), - ['PUBSUB', 'CHANNELS', 'patter*'] - ); - }); + it('with pattern', () => { + assert.deepEqual( + PUBSUB_CHANNELS.transformArguments('patter*'), + ['PUBSUB', 'CHANNELS', 'patter*'] + ); }); + }); - testUtils.testWithClient('client.pubSubChannels', async client => { - assert.deepEqual( - await client.pubSubChannels(), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.pubSubChannels', async client => { + assert.deepEqual( + await client.pubSubChannels(), + [] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/PUBSUB_CHANNELS.ts b/packages/client/lib/commands/PUBSUB_CHANNELS.ts index 86a144ede8e..4bf7abd75dc 100644 --- a/packages/client/lib/commands/PUBSUB_CHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_CHANNELS.ts @@ -1,13 +1,17 @@ -export const IS_READ_ONLY = true; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export function transformArguments(pattern?: string): Array { - const args = ['PUBSUB', 'CHANNELS']; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(pattern?: RedisArgument) { + const args: Array = ['PUBSUB', 'CHANNELS']; if (pattern) { - args.push(pattern); + args.push(pattern); } return args; -} + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; -export declare function transformReply(): Array; diff --git a/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts b/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts index d738b916c60..43a2b4b5c0e 100644 --- a/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts +++ b/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PUBSUB_NUMPAT'; +import PUBSUB_NUMPAT from './PUBSUB_NUMPAT'; describe('PUBSUB NUMPAT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['PUBSUB', 'NUMPAT'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + PUBSUB_NUMPAT.transformArguments(), + ['PUBSUB', 'NUMPAT'] + ); + }); - testUtils.testWithClient('client.pubSubNumPat', async client => { - assert.equal( - await client.pubSubNumPat(), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.pubSubNumPat', async client => { + assert.equal( + await client.pubSubNumPat(), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/PUBSUB_NUMPAT.ts b/packages/client/lib/commands/PUBSUB_NUMPAT.ts index 15be6aa1b18..e8a0738dc72 100644 --- a/packages/client/lib/commands/PUBSUB_NUMPAT.ts +++ b/packages/client/lib/commands/PUBSUB_NUMPAT.ts @@ -1,7 +1,10 @@ -export const IS_READ_ONLY = true; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['PUBSUB', 'NUMPAT']; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts b/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts index e35558ef865..151dc219288 100644 --- a/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts +++ b/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts @@ -1,35 +1,35 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PUBSUB_NUMSUB'; +import PUBSUB_NUMSUB from './PUBSUB_NUMSUB'; describe('PUBSUB NUMSUB', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['PUBSUB', 'NUMSUB'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + PUBSUB_NUMSUB.transformArguments(), + ['PUBSUB', 'NUMSUB'] + ); + }); - it('string', () => { - assert.deepEqual( - transformArguments('channel'), - ['PUBSUB', 'NUMSUB', 'channel'] - ); - }); + it('string', () => { + assert.deepEqual( + PUBSUB_NUMSUB.transformArguments('channel'), + ['PUBSUB', 'NUMSUB', 'channel'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['PUBSUB', 'NUMSUB', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + PUBSUB_NUMSUB.transformArguments(['1', '2']), + ['PUBSUB', 'NUMSUB', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.pubSubNumSub', async client => { - assert.deepEqual( - await client.pubSubNumSub(), - Object.create(null) - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.pubSubNumSub', async client => { + assert.deepEqual( + await client.pubSubNumSub(), + Object.create(null) + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/PUBSUB_NUMSUB.ts b/packages/client/lib/commands/PUBSUB_NUMSUB.ts index f47238f8882..1f7c41f5bdd 100644 --- a/packages/client/lib/commands/PUBSUB_NUMSUB.ts +++ b/packages/client/lib/commands/PUBSUB_NUMSUB.ts @@ -1,24 +1,23 @@ -import { pushVerdictArguments } from './generic-transformers'; -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments( - channels?: Array | RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(channels?: RedisVariadicArgument) { const args = ['PUBSUB', 'NUMSUB']; - if (channels) return pushVerdictArguments(args, channels); + if (channels) return pushVariadicArguments(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]; + }, + transformReply(rawReply: UnwrapReply>) { + const reply = Object.create(null); + let i = 0; + while (i < rawReply.length) { + reply[rawReply[i++].toString()] = rawReply[i++].toString(); } - return transformedReply; -} + return reply as Record; + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts index 1e5f2292b39..77982b34670 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts @@ -1,30 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PUBSUB_SHARDCHANNELS'; +import PUBSUB_SHARDCHANNELS from './PUBSUB_SHARDCHANNELS'; describe('PUBSUB SHARDCHANNELS', () => { - testUtils.isVersionGreaterThanHook([7]); - - describe('transformArguments', () => { - it('without pattern', () => { - assert.deepEqual( - transformArguments(), - ['PUBSUB', 'SHARDCHANNELS'] - ); - }); + testUtils.isVersionGreaterThanHook([7]); - it('with pattern', () => { - assert.deepEqual( - transformArguments('patter*'), - ['PUBSUB', 'SHARDCHANNELS', 'patter*'] - ); - }); + describe('transformArguments', () => { + it('without pattern', () => { + assert.deepEqual( + PUBSUB_SHARDCHANNELS.transformArguments(), + ['PUBSUB', 'SHARDCHANNELS'] + ); }); - testUtils.testWithClient('client.pubSubShardChannels', async client => { - assert.deepEqual( - await client.pubSubShardChannels(), - [] - ); - }, GLOBAL.SERVERS.OPEN); + it('with pattern', () => { + assert.deepEqual( + PUBSUB_SHARDCHANNELS.transformArguments('patter*'), + ['PUBSUB', 'SHARDCHANNELS', 'patter*'] + ); + }); + }); + + testUtils.testWithClient('client.pubSubShardChannels', async client => { + assert.deepEqual( + await client.pubSubShardChannels(), + [] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts index e998677848a..74d78c02614 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts @@ -1,13 +1,16 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(pattern?: RedisArgument) { + const args: Array = ['PUBSUB', 'SHARDCHANNELS']; -export function transformArguments( - pattern?: RedisCommandArgument -): RedisCommandArguments { - const args: RedisCommandArguments = ['PUBSUB', 'SHARDCHANNELS']; - if (pattern) args.push(pattern); - return args; -} + if (pattern) { + args.push(pattern); + } -export declare function transformReply(): Array; + return args; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts index fea1373b55d..e036a6eae5b 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts @@ -1,48 +1,48 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PUBSUB_SHARDNUMSUB'; +import PUBSUB_SHARDNUMSUB from './PUBSUB_SHARDNUMSUB'; describe('PUBSUB SHARDNUMSUB', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['PUBSUB', 'SHARDNUMSUB'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + PUBSUB_SHARDNUMSUB.transformArguments(), + ['PUBSUB', 'SHARDNUMSUB'] + ); + }); - it('string', () => { - assert.deepEqual( - transformArguments('channel'), - ['PUBSUB', 'SHARDNUMSUB', 'channel'] - ); - }); + it('string', () => { + assert.deepEqual( + PUBSUB_SHARDNUMSUB.transformArguments('channel'), + ['PUBSUB', 'SHARDNUMSUB', 'channel'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['PUBSUB', 'SHARDNUMSUB', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + PUBSUB_SHARDNUMSUB.transformArguments(['1', '2']), + ['PUBSUB', 'SHARDNUMSUB', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.pubSubShardNumSub', async client => { - assert.deepEqual( - await client.pubSubShardNumSub(['foo', 'bar']), - Object.create(null, { - foo: { - value: 0, - configurable: true, - enumerable: true - }, - bar: { - value: 0, - configurable: true, - enumerable: true - } - }) - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.pubSubShardNumSub', async client => { + assert.deepEqual( + await client.pubSubShardNumSub(['foo', 'bar']), + Object.create(null, { + foo: { + value: 0, + configurable: true, + enumerable: true + }, + bar: { + value: 0, + configurable: true, + enumerable: true + } + }) + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts index 4d7f4d8a71e..0ef82477006 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts @@ -1,24 +1,24 @@ -import { pushVerdictArguments } from './generic-transformers'; -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments( - channels?: Array | RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(channels?: RedisVariadicArgument) { const args = ['PUBSUB', 'SHARDNUMSUB']; - if (channels) return pushVerdictArguments(args, channels); + if (channels) return pushVariadicArguments(args, channels); return args; -} - -export function transformReply(rawReply: Array): Record { - const transformedReply = Object.create(null); + }, + transformReply(reply: UnwrapReply>) { + const transformedReply: Record = Object.create(null); - for (let i = 0; i < rawReply.length; i += 2) { - transformedReply[rawReply[i]] = rawReply[i + 1]; + for (let i = 0; i < reply.length; i += 2) { + transformedReply[(reply[i] as BlobStringReply).toString()] = reply[i + 1] as NumberReply; } - + return transformedReply; -} + } +} as const satisfies Command; + diff --git a/packages/client/lib/commands/RANDOMKEY.spec.ts b/packages/client/lib/commands/RANDOMKEY.spec.ts index 81c42b2fd83..31de60d7a99 100644 --- a/packages/client/lib/commands/RANDOMKEY.spec.ts +++ b/packages/client/lib/commands/RANDOMKEY.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RANDOMKEY'; +import RANDOMKEY from './RANDOMKEY'; describe('RANDOMKEY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['RANDOMKEY'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + RANDOMKEY.transformArguments(), + ['RANDOMKEY'] + ); + }); - testUtils.testWithClient('client.randomKey', async client => { - assert.equal( - await client.randomKey(), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('randomKey', async client => { + assert.equal( + await client.randomKey(), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/RANDOMKEY.ts b/packages/client/lib/commands/RANDOMKEY.ts index f2d511d4dec..42028ebbe38 100644 --- a/packages/client/lib/commands/RANDOMKEY.ts +++ b/packages/client/lib/commands/RANDOMKEY.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { NumberReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['RANDOMKEY']; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/READONLY.spec.ts b/packages/client/lib/commands/READONLY.spec.ts index aa4db47f81a..14bb047349a 100644 --- a/packages/client/lib/commands/READONLY.spec.ts +++ b/packages/client/lib/commands/READONLY.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './READONLY'; +import { strict as assert } from 'node:assert'; +import READONLY from './READONLY'; describe('READONLY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['READONLY'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + READONLY.transformArguments(), + ['READONLY'] + ); + }); }); diff --git a/packages/client/lib/commands/READONLY.ts b/packages/client/lib/commands/READONLY.ts index db7db881628..bb15834550e 100644 --- a/packages/client/lib/commands/READONLY.ts +++ b/packages/client/lib/commands/READONLY.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['READONLY']; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['READONLY']; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/READWRITE.spec.ts b/packages/client/lib/commands/READWRITE.spec.ts index 6ce4a3ee56a..94a88a0dcc7 100644 --- a/packages/client/lib/commands/READWRITE.spec.ts +++ b/packages/client/lib/commands/READWRITE.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './READWRITE'; +import { strict as assert } from 'node:assert'; +import READWRITE from './READWRITE'; describe('READWRITE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['READWRITE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + READWRITE.transformArguments(), + ['READWRITE'] + ); + }); }); diff --git a/packages/client/lib/commands/READWRITE.ts b/packages/client/lib/commands/READWRITE.ts index 60dc865e89e..fe70e15d4c8 100644 --- a/packages/client/lib/commands/READWRITE.ts +++ b/packages/client/lib/commands/READWRITE.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['READWRITE']; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['READWRITE']; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/RENAME.spec.ts b/packages/client/lib/commands/RENAME.spec.ts index 49e0af600f6..cf3dccbf3e7 100644 --- a/packages/client/lib/commands/RENAME.spec.ts +++ b/packages/client/lib/commands/RENAME.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RENAME'; +import RENAME from './RENAME'; describe('RENAME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('from', 'to'), - ['RENAME', 'from', 'to'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + RENAME.transformArguments('source', 'destination'), + ['RENAME', 'source', 'destination'] + ); + }); - testUtils.testWithClient('client.rename', async client => { - await client.set('from', 'value'); - - assert.equal( - await client.rename('from', 'to'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('rename', async client => { + const [, reply] = await Promise.all([ + client.set('{tag}source', 'value'), + client.rename('{tag}source', '{tag}destination') + ]); + + assert.equal(reply, 'OK'); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/RENAME.ts b/packages/client/lib/commands/RENAME.ts index 2d1134084fb..16e883d0532 100644 --- a/packages/client/lib/commands/RENAME.ts +++ b/packages/client/lib/commands/RENAME.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - newKey: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, newKey: RedisArgument) { return ['RENAME', key, newKey]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/RENAMENX.spec.ts b/packages/client/lib/commands/RENAMENX.spec.ts index 6345eb5bd09..5f83933e957 100644 --- a/packages/client/lib/commands/RENAMENX.spec.ts +++ b/packages/client/lib/commands/RENAMENX.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RENAMENX'; +import RENAMENX from './RENAMENX'; describe('RENAMENX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('from', 'to'), - ['RENAMENX', 'from', 'to'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + RENAMENX.transformArguments('source', 'destination'), + ['RENAMENX', 'source', 'destination'] + ); + }); - testUtils.testWithClient('client.renameNX', async client => { - await client.set('from', 'value'); + testUtils.testAll('renameNX', async client => { + const [, reply] = await Promise.all([ + client.set('{tag}source', 'value'), + client.renameNX('{tag}source', '{tag}destination') + ]); - assert.equal( - await client.renameNX('from', 'to'), - true - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 1); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/RENAMENX.ts b/packages/client/lib/commands/RENAMENX.ts index 322ff0a88cc..3a4f155d5a7 100644 --- a/packages/client/lib/commands/RENAMENX.ts +++ b/packages/client/lib/commands/RENAMENX.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - newKey: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, newKey: RedisArgument) { return ['RENAMENX', key, newKey]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/REPLICAOF.spec.ts b/packages/client/lib/commands/REPLICAOF.spec.ts index ab1906944cf..77dbe060450 100644 --- a/packages/client/lib/commands/REPLICAOF.spec.ts +++ b/packages/client/lib/commands/REPLICAOF.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './REPLICAOF'; +import { strict as assert } from 'node:assert'; +import REPLICAOF from './REPLICAOF'; describe('REPLICAOF', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('host', 1), - ['REPLICAOF', 'host', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + REPLICAOF.transformArguments('host', 1), + ['REPLICAOF', 'host', '1'] + ); + }); }); diff --git a/packages/client/lib/commands/REPLICAOF.ts b/packages/client/lib/commands/REPLICAOF.ts index bd452e0f371..4e2f69f7265 100644 --- a/packages/client/lib/commands/REPLICAOF.ts +++ b/packages/client/lib/commands/REPLICAOF.ts @@ -1,5 +1,10 @@ -export function transformArguments(host: string, port: number): Array { - return ['REPLICAOF', host, port.toString()]; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(host: string, port: number) { + return ['REPLICAOF', host, port.toString()]; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/RESTORE-ASKING.spec.ts b/packages/client/lib/commands/RESTORE-ASKING.spec.ts index de9fce5c628..855196b9776 100644 --- a/packages/client/lib/commands/RESTORE-ASKING.spec.ts +++ b/packages/client/lib/commands/RESTORE-ASKING.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './RESTORE-ASKING'; +import { strict as assert } from 'node:assert'; +import RESTORE_ASKING from './RESTORE-ASKING'; describe('RESTORE-ASKING', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['RESTORE-ASKING'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + RESTORE_ASKING.transformArguments(), + ['RESTORE-ASKING'] + ); + }); }); diff --git a/packages/client/lib/commands/RESTORE-ASKING.ts b/packages/client/lib/commands/RESTORE-ASKING.ts index d53d8541cd7..14f6dcbeab3 100644 --- a/packages/client/lib/commands/RESTORE-ASKING.ts +++ b/packages/client/lib/commands/RESTORE-ASKING.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['RESTORE-ASKING']; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['RESTORE-ASKING']; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/RESTORE.spec.ts b/packages/client/lib/commands/RESTORE.spec.ts index 89d42f3d4de..6b814e7325a 100644 --- a/packages/client/lib/commands/RESTORE.spec.ts +++ b/packages/client/lib/commands/RESTORE.spec.ts @@ -1,74 +1,84 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RESTORE'; +import RESTORE from './RESTORE'; +import { RESP_TYPES } from '../RESP/decoder'; describe('RESTORE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 0, 'value'), - ['RESTORE', 'key', '0', 'value'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + RESTORE.transformArguments('key', 0, 'value'), + ['RESTORE', 'key', '0', 'value'] + ); + }); - it('with REPLACE', () => { - assert.deepEqual( - transformArguments('key', 0, 'value', { - REPLACE: true - }), - ['RESTORE', 'key', '0', 'value', 'REPLACE'] - ); - }); + it('with REPLACE', () => { + assert.deepEqual( + RESTORE.transformArguments('key', 0, 'value', { + REPLACE: true + }), + ['RESTORE', 'key', '0', 'value', 'REPLACE'] + ); + }); - it('with ABSTTL', () => { - assert.deepEqual( - transformArguments('key', 0, 'value', { - ABSTTL: true - }), - ['RESTORE', 'key', '0', 'value', 'ABSTTL'] - ); - }); + it('with ABSTTL', () => { + assert.deepEqual( + RESTORE.transformArguments('key', 0, 'value', { + ABSTTL: true + }), + ['RESTORE', 'key', '0', 'value', 'ABSTTL'] + ); + }); - it('with IDLETIME', () => { - assert.deepEqual( - transformArguments('key', 0, 'value', { - IDLETIME: 1 - }), - ['RESTORE', 'key', '0', 'value', 'IDLETIME', '1'] - ); - }); + it('with IDLETIME', () => { + assert.deepEqual( + RESTORE.transformArguments('key', 0, 'value', { + IDLETIME: 1 + }), + ['RESTORE', 'key', '0', 'value', 'IDLETIME', '1'] + ); + }); - it('with FREQ', () => { - assert.deepEqual( - transformArguments('key', 0, 'value', { - FREQ: 1 - }), - ['RESTORE', 'key', '0', 'value', 'FREQ', '1'] - ); - }); + it('with FREQ', () => { + assert.deepEqual( + RESTORE.transformArguments('key', 0, 'value', { + FREQ: 1 + }), + ['RESTORE', 'key', '0', 'value', 'FREQ', '1'] + ); + }); - it('with REPLACE, ABSTTL, IDLETIME and FREQ', () => { - assert.deepEqual( - transformArguments('key', 0, 'value', { - REPLACE: true, - ABSTTL: true, - IDLETIME: 1, - FREQ: 2 - }), - ['RESTORE', 'key', '0', 'value', 'REPLACE', 'ABSTTL', 'IDLETIME', '1', 'FREQ', '2'] - ); - }); + it('with REPLACE, ABSTTL, IDLETIME and FREQ', () => { + assert.deepEqual( + RESTORE.transformArguments('key', 0, 'value', { + REPLACE: true, + ABSTTL: true, + IDLETIME: 1, + FREQ: 2 + }), + ['RESTORE', 'key', '0', 'value', 'REPLACE', 'ABSTTL', 'IDLETIME', '1', 'FREQ', '2'] + ); }); + }); - testUtils.testWithClient('client.restore', async client => { - const [, dump] = await Promise.all([ - client.set('source', 'value'), - client.dump(client.commandOptions({ returnBuffers: true }), 'source') - ]); + testUtils.testWithClient('client.restore', async client => { + const [, dump] = await Promise.all([ + client.set('source', 'value'), + client.dump('source') + ]); - assert.equal( - await client.restore('destination', 0, dump), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal( + await client.restore('destination', 0, dump), + 'OK' + ); + }, { + ...GLOBAL.SERVERS.OPEN, + clientOptions: { + commandOptions: { + typeMapping: { + [RESP_TYPES.BLOB_STRING]: Buffer + } + } + } + }); }); diff --git a/packages/client/lib/commands/RESTORE.ts b/packages/client/lib/commands/RESTORE.ts index d9ac11c424b..b24c5b569f9 100644 --- a/packages/client/lib/commands/RESTORE.ts +++ b/packages/client/lib/commands/RESTORE.ts @@ -1,39 +1,40 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -interface RestoreOptions { - REPLACE?: true; - ABSTTL?: true; - IDLETIME?: number; - FREQ?: number; +export interface RestoreOptions { + REPLACE?: boolean; + ABSTTL?: boolean; + IDLETIME?: number; + FREQ?: number; } -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, ttl: number, - serializedValue: RedisCommandArgument, + serializedValue: RedisArgument, options?: RestoreOptions -): RedisCommandArguments { + ) { const args = ['RESTORE', key, ttl.toString(), serializedValue]; if (options?.REPLACE) { - args.push('REPLACE'); + args.push('REPLACE'); } if (options?.ABSTTL) { - args.push('ABSTTL'); + args.push('ABSTTL'); } if (options?.IDLETIME) { - args.push('IDLETIME', options.IDLETIME.toString()); + args.push('IDLETIME', options.IDLETIME.toString()); } if (options?.FREQ) { - args.push('FREQ', options.FREQ.toString()); + args.push('FREQ', options.FREQ.toString()); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/ROLE.spec.ts b/packages/client/lib/commands/ROLE.spec.ts index 2e6d9b163ae..c57ba5ba1f0 100644 --- a/packages/client/lib/commands/ROLE.spec.ts +++ b/packages/client/lib/commands/ROLE.spec.ts @@ -1,69 +1,69 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './ROLE'; +import ROLE from './ROLE'; describe('ROLE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['ROLE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ROLE.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 - }] - } - ); - }); + describe('transformReply', () => { + it('master', () => { + assert.deepEqual( + ROLE.transformReply(['master', 3129659, [['127.0.0.1', '9001', '3129242'], ['127.0.0.1', '9002', '3129543']]] as any), + { + role: 'master', + replicationOffest: 3129659, + replicas: [{ + host: '127.0.0.1', + port: 9001, + replicationOffest: 3129242 + }, { + host: '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('replica', () => { + assert.deepEqual( + ROLE.transformReply(['slave', '127.0.0.1', 9000, 'connected', 3167038] as any), + { + role: 'slave', + master: { + host: '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'] - } - ); - }); + it('sentinel', () => { + assert.deepEqual( + ROLE.transformReply(['sentinel', ['resque-master', 'html-fragments-master', 'stats-master', 'metadata-master']] as any), + { + role: 'sentinel', + masterNames: ['resque-master', 'html-fragments-master', 'stats-master', 'metadata-master'] + } + ); }); + }); - testUtils.testWithClient('client.role', async client => { - assert.deepEqual( - await client.role(), - { - role: 'master', - replicationOffest: 0, - replicas: [] - } - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.role', async client => { + assert.deepEqual( + await client.role(), + { + role: 'master', + replicationOffest: 0, + replicas: [] + } + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ROLE.ts b/packages/client/lib/commands/ROLE.ts index b1d6041fdfa..7828e53fb61 100644 --- a/packages/client/lib/commands/ROLE.ts +++ b/packages/client/lib/commands/ROLE.ts @@ -1,75 +1,70 @@ -export const IS_READ_ONLY = true; +import { BlobStringReply, NumberReply, ArrayReply, TuplesReply, UnwrapReply, Command } from '../RESP/types'; -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 MasterRole = [ + role: BlobStringReply<'master'>, + replicationOffest: NumberReply, + replicas: ArrayReply> +]; -type RoleSentinelRawReply = ['sentinel', Array]; +type SlaveRole = [ + role: BlobStringReply<'slave'>, + masterHost: BlobStringReply, + masterPort: NumberReply, + state: BlobStringReply<'connect' | 'connecting' | 'sync' | 'connected'>, + dataReceived: NumberReply +]; -interface RoleSentinelReply extends RoleReplyInterface<'sentinel'> { - masterNames: Array; -} +type SentinelRole = [ + role: BlobStringReply<'sentinel'>, + masterNames: ArrayReply +]; -type RoleRawReply = RoleMasterRawReply | RoleReplicaRawReply | RoleSentinelRawReply; +type Role = TuplesReply; -type RoleReply = RoleMasterReply | RoleReplicaReply | RoleSentinelReply; - -export function transformReply(reply: RoleRawReply): RoleReply { - switch (reply[0]) { - case 'master': +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['ROLE']; + }, + transformReply(reply: UnwrapReply) { + switch (reply[0] as unknown as UnwrapReply) { + case 'master': { + const [role, replicationOffest, replicas] = reply as MasterRole; + return { + role, + replicationOffest, + replicas: (replicas as unknown as UnwrapReply).map(replica => { + const [host, port, replicationOffest] = replica as unknown as UnwrapReply; return { - role: 'master', - replicationOffest: reply[1], - replicas: reply[2].map(([ip, port, replicationOffest]) => ({ - ip, - port: Number(port), - replicationOffest: Number(replicationOffest) - })) + host, + 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 'slave': { + const [role, masterHost, masterPort, state, dataReceived] = reply as SlaveRole; + return { + role, + master: { + host: masterHost, + port: masterPort + }, + state, + dataReceived, + }; + } - case 'sentinel': - return { - role: 'sentinel', - masterNames: reply[1] - }; + case 'sentinel': { + const [role, masterNames] = reply as SentinelRole; + return { + role, + masterNames + }; + } } -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/RPOP.spec.ts b/packages/client/lib/commands/RPOP.spec.ts index 6e57afa3216..8ac5cb290f4 100644 --- a/packages/client/lib/commands/RPOP.spec.ts +++ b/packages/client/lib/commands/RPOP.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RPOP'; +import RPOP from './RPOP'; describe('RPOP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['RPOP', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + RPOP.transformArguments('key'), + ['RPOP', 'key'] + ); + }); - testUtils.testWithClient('client.rPop', async client => { - assert.equal( - await client.rPop('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.rPop', async cluster => { - assert.equal( - await cluster.rPop('key'), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('rPop', async client => { + assert.equal( + await client.rPop('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/RPOP.ts b/packages/client/lib/commands/RPOP.ts index ed696b6d522..f7d0b33d3af 100644 --- a/packages/client/lib/commands/RPOP.ts +++ b/packages/client/lib/commands/RPOP.ts @@ -1,9 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument) { return ['RPOP', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/RPOPLPUSH.spec.ts b/packages/client/lib/commands/RPOPLPUSH.spec.ts index cef3049bd91..59458fc0aa8 100644 --- a/packages/client/lib/commands/RPOPLPUSH.spec.ts +++ b/packages/client/lib/commands/RPOPLPUSH.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RPOPLPUSH'; +import RPOPLPUSH from './RPOPLPUSH'; describe('RPOPLPUSH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination'), - ['RPOPLPUSH', 'source', 'destination'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + RPOPLPUSH.transformArguments('source', 'destination'), + ['RPOPLPUSH', 'source', 'destination'] + ); + }); - testUtils.testWithClient('client.rPopLPush', async client => { - assert.equal( - await client.rPopLPush('source', 'destination'), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.rPopLPush', async cluster => { - assert.equal( - await cluster.rPopLPush('{tag}source', '{tag}destination'), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('rPopLPush', async client => { + assert.equal( + await client.rPopLPush('{tag}source', '{tag}destination'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/RPOPLPUSH.ts b/packages/client/lib/commands/RPOPLPUSH.ts index da45f6f6024..1a5e1cc59bc 100644 --- a/packages/client/lib/commands/RPOPLPUSH.ts +++ b/packages/client/lib/commands/RPOPLPUSH.ts @@ -1,12 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + source: RedisArgument, + destination: RedisArgument + ) { return ['RPOPLPUSH', source, destination]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/RPOP_COUNT.spec.ts b/packages/client/lib/commands/RPOP_COUNT.spec.ts index 3657a608039..14f1854b8bc 100644 --- a/packages/client/lib/commands/RPOP_COUNT.spec.ts +++ b/packages/client/lib/commands/RPOP_COUNT.spec.ts @@ -1,28 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RPOP_COUNT'; +import RPOP_COUNT from './RPOP_COUNT'; describe('RPOP COUNT', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['RPOP', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + RPOP_COUNT.transformArguments('key', 1), + ['RPOP', 'key', '1'] + ); + }); - testUtils.testWithClient('client.rPopCount', async client => { - assert.equal( - await client.rPopCount('key', 1), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.rPopCount', async cluster => { - assert.equal( - await cluster.rPopCount('key', 1), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('rPopCount', async client => { + assert.equal( + await client.rPopCount('key', 1), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/RPOP_COUNT.ts b/packages/client/lib/commands/RPOP_COUNT.ts index b3bc778ee5c..b60dec6ab9d 100644 --- a/packages/client/lib/commands/RPOP_COUNT.ts +++ b/packages/client/lib/commands/RPOP_COUNT.ts @@ -1,12 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, count: number) { return ['RPOP', key, count.toString()]; -} - -export declare function transformReply(): Array | null; + }, + transformReply: undefined as unknown as () => ArrayReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/RPUSH.spec.ts b/packages/client/lib/commands/RPUSH.spec.ts index afa5c1c6400..06078d75951 100644 --- a/packages/client/lib/commands/RPUSH.spec.ts +++ b/packages/client/lib/commands/RPUSH.spec.ts @@ -1,35 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RPUSH'; +import RPUSH 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'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + RPUSH.transformArguments('key', 'element'), + ['RPUSH', 'key', 'element'] + ); }); - testUtils.testWithClient('client.rPush', async client => { - assert.equal( - await client.rPush('key', 'element'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + it('array', () => { + assert.deepEqual( + RPUSH.transformArguments('key', ['1', '2']), + ['RPUSH', 'key', '1', '2'] + ); + }); + }); - testUtils.testWithCluster('cluster.rPush', async cluster => { - assert.equal( - await cluster.rPush('key', 'element'), - 1 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('rPush', async client => { + assert.equal( + await client.rPush('key', 'element'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/RPUSH.ts b/packages/client/lib/commands/RPUSH.ts index 15e282f0892..4b048777389 100644 --- a/packages/client/lib/commands/RPUSH.ts +++ b/packages/client/lib/commands/RPUSH.ts @@ -1,13 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - element: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['RPUSH', key], element); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + element: RedisVariadicArgument + ) { + return pushVariadicArguments(['RPUSH', key], element); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/RPUSHX.spec.ts b/packages/client/lib/commands/RPUSHX.spec.ts index ee2041de6f2..5adaa8b263a 100644 --- a/packages/client/lib/commands/RPUSHX.spec.ts +++ b/packages/client/lib/commands/RPUSHX.spec.ts @@ -1,35 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RPUSHX'; +import RPUSHX 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'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + RPUSHX.transformArguments('key', 'element'), + ['RPUSHX', 'key', 'element'] + ); }); - testUtils.testWithClient('client.rPushX', async client => { - assert.equal( - await client.rPushX('key', 'element'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + it('array', () => { + assert.deepEqual( + RPUSHX.transformArguments('key', ['1', '2']), + ['RPUSHX', 'key', '1', '2'] + ); + }); + }); - testUtils.testWithCluster('cluster.rPushX', async cluster => { - assert.equal( - await cluster.rPushX('key', 'element'), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('rPushX', async client => { + assert.equal( + await client.rPushX('key', 'element'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/RPUSHX.ts b/packages/client/lib/commands/RPUSHX.ts index 29253cd6edb..00b29624b0d 100644 --- a/packages/client/lib/commands/RPUSHX.ts +++ b/packages/client/lib/commands/RPUSHX.ts @@ -1,13 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - element: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['RPUSHX', key], element); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + element: RedisVariadicArgument + ) { + return pushVariadicArguments(['RPUSHX', key], element); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SADD.spec.ts b/packages/client/lib/commands/SADD.spec.ts index 4533f6f9ad5..77adc1c18ce 100644 --- a/packages/client/lib/commands/SADD.spec.ts +++ b/packages/client/lib/commands/SADD.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SADD'; +import SADD from './SADD'; describe('SADD', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['SADD', 'key', 'member'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + SADD.transformArguments('key', 'member'), + ['SADD', 'key', 'member'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['SADD', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + SADD.transformArguments('key', ['1', '2']), + ['SADD', 'key', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.sAdd', async client => { - assert.equal( - await client.sAdd('key', 'member'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sAdd', async client => { + assert.equal( + await client.sAdd('key', 'member'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SADD.ts b/packages/client/lib/commands/SADD.ts index 7d7121e5391..2ff5e9263c3 100644 --- a/packages/client/lib/commands/SADD.ts +++ b/packages/client/lib/commands/SADD.ts @@ -1,13 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - members: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['SADD', key], members); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, members: RedisVariadicArgument) { + return pushVariadicArguments(['SADD', key], members); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SAVE.spec.ts b/packages/client/lib/commands/SAVE.spec.ts index 1e1987b5ab8..5c014da7edb 100644 --- a/packages/client/lib/commands/SAVE.spec.ts +++ b/packages/client/lib/commands/SAVE.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './SAVE'; +import { strict as assert } from 'node:assert'; +import SAVE from './SAVE'; describe('SAVE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['SAVE'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SAVE.transformArguments(), + ['SAVE'] + ); + }); }); diff --git a/packages/client/lib/commands/SAVE.ts b/packages/client/lib/commands/SAVE.ts index 3d75c29df90..ee6cccd35a0 100644 --- a/packages/client/lib/commands/SAVE.ts +++ b/packages/client/lib/commands/SAVE.ts @@ -1,7 +1,10 @@ -import { RedisCommandArgument } from '.'; +import { SimpleStringReply, Command } from '../RESP/types'; -export function transformArguments(): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['SAVE']; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SCAN.spec.ts b/packages/client/lib/commands/SCAN.spec.ts index 7657b744e02..f4dd865d113 100644 --- a/packages/client/lib/commands/SCAN.spec.ts +++ b/packages/client/lib/commands/SCAN.spec.ts @@ -1,84 +1,62 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './SCAN'; +import SCAN 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'] - ); - }); + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + SCAN.transformArguments('0'), + ['SCAN', '0'] + ); + }); - it('with MATCH & COUNT & TYPE', () => { - assert.deepEqual( - transformArguments(0, { - MATCH: 'pattern', - COUNT: 1, - TYPE: 'stream' - }), - ['SCAN', '0', 'MATCH', 'pattern', 'COUNT', '1', 'TYPE', 'stream'] - ); - }); + it('with MATCH', () => { + assert.deepEqual( + SCAN.transformArguments('0', { + MATCH: 'pattern' + }), + ['SCAN', '0', 'MATCH', 'pattern'] + ); }); - describe('transformReply', () => { - it('without keys', () => { - assert.deepEqual( - transformReply(['0', []]), - { - cursor: 0, - keys: [] - } - ); - }); + it('with COUNT', () => { + assert.deepEqual( + SCAN.transformArguments('0', { + COUNT: 1 + }), + ['SCAN', '0', 'COUNT', '1'] + ); + }); - it('with keys', () => { - assert.deepEqual( - transformReply(['0', ['key']]), - { - cursor: 0, - keys: ['key'] - } - ); - }); + it('with TYPE', () => { + assert.deepEqual( + SCAN.transformArguments('0', { + TYPE: 'stream' + }), + ['SCAN', '0', 'TYPE', 'stream'] + ); }); - testUtils.testWithClient('client.scan', async client => { - assert.deepEqual( - await client.scan(0), - { - cursor: 0, - keys: [] - } - ); - }, GLOBAL.SERVERS.OPEN); + it('with MATCH & COUNT & TYPE', () => { + assert.deepEqual( + SCAN.transformArguments('0', { + MATCH: 'pattern', + COUNT: 1, + TYPE: 'stream' + }), + ['SCAN', '0', 'MATCH', 'pattern', 'COUNT', '1', 'TYPE', 'stream'] + ); + }); + }); + + testUtils.testWithClient('client.scan', async client => { + assert.deepEqual( + await client.scan('0'), + { + cursor: '0', + keys: [] + } + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/SCAN.ts b/packages/client/lib/commands/SCAN.ts index ee5908eb9bd..13f54440443 100644 --- a/packages/client/lib/commands/SCAN.ts +++ b/packages/client/lib/commands/SCAN.ts @@ -1,34 +1,48 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions, pushScanArguments } from './generic-transformers'; +import { RedisArgument, CommandArguments, BlobStringReply, ArrayReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; -export interface ScanCommandOptions extends ScanOptions { - TYPE?: RedisCommandArgument; +export interface ScanCommonOptions { + MATCH?: string; + COUNT?: number; } -export function transformArguments( - cursor: number, - options?: ScanCommandOptions -): RedisCommandArguments { - const args = pushScanArguments(['SCAN'], cursor, options); +export function pushScanArguments( + args: CommandArguments, + cursor: RedisArgument, + options?: ScanOptions +): CommandArguments { + args.push(cursor.toString()); - if (options?.TYPE) { - args.push('TYPE', options.TYPE); - } + if (options?.MATCH) { + args.push('MATCH', options.MATCH); + } - return args; -} + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } -type ScanRawReply = [string, Array]; + return args; +} -export interface ScanReply { - cursor: number; - keys: Array; +export interface ScanOptions extends ScanCommonOptions { + TYPE?: RedisArgument; } -export function transformReply([cursor, keys]: ScanRawReply): ScanReply { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(cursor: RedisArgument, options?: ScanOptions) { + const args = pushScanArguments(['SCAN'], cursor, options); + + if (options?.TYPE) { + args.push('TYPE', options.TYPE); + } + + return args; + }, + transformReply([cursor, keys]: [BlobStringReply, ArrayReply]) { return { - cursor: Number(cursor), - keys + cursor, + keys }; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/SCARD.spec.ts b/packages/client/lib/commands/SCARD.spec.ts index afc21c6b00c..5029f340d96 100644 --- a/packages/client/lib/commands/SCARD.spec.ts +++ b/packages/client/lib/commands/SCARD.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SCARD'; +import SCARD from './SCARD'; describe('SCARD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['SCARD', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SCARD.transformArguments('key'), + ['SCARD', 'key'] + ); + }); - testUtils.testWithClient('client.sCard', async client => { - assert.equal( - await client.sCard('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sCard', async client => { + assert.equal( + await client.sCard('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SCARD.ts b/packages/client/lib/commands/SCARD.ts index 0d3ce49b6b2..c13d042ba60 100644 --- a/packages/client/lib/commands/SCARD.ts +++ b/packages/client/lib/commands/SCARD.ts @@ -1,7 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export function transformArguments(key: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['SCARD', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts b/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts index 192f90f75a5..4e07f2c250c 100644 --- a/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts +++ b/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SCRIPT_DEBUG'; +import SCRIPT_DEBUG from './SCRIPT_DEBUG'; describe('SCRIPT DEBUG', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('NO'), - ['SCRIPT', 'DEBUG', 'NO'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SCRIPT_DEBUG.transformArguments('NO'), + ['SCRIPT', 'DEBUG', 'NO'] + ); + }); - testUtils.testWithClient('client.scriptDebug', async client => { - assert.equal( - await client.scriptDebug('NO'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.scriptDebug', async client => { + assert.equal( + await client.scriptDebug('NO'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/SCRIPT_DEBUG.ts b/packages/client/lib/commands/SCRIPT_DEBUG.ts index e9e1e909d59..3c49ff709d5 100644 --- a/packages/client/lib/commands/SCRIPT_DEBUG.ts +++ b/packages/client/lib/commands/SCRIPT_DEBUG.ts @@ -1,5 +1,10 @@ -export function transformArguments(mode: 'YES' | 'SYNC' | 'NO'): Array { - return ['SCRIPT', 'DEBUG', mode]; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(mode: 'YES' | 'SYNC' | 'NO') { + return ['SCRIPT', 'DEBUG', mode]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts b/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts index e0fbbcc5537..8afdbb5f581 100644 --- a/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts +++ b/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts @@ -1,28 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SCRIPT_EXISTS'; +import SCRIPT_EXISTS from './SCRIPT_EXISTS'; describe('SCRIPT EXISTS', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('sha1'), - ['SCRIPT', 'EXISTS', 'sha1'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + SCRIPT_EXISTS.transformArguments('sha1'), + ['SCRIPT', 'EXISTS', 'sha1'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['SCRIPT', 'EXISTS', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + SCRIPT_EXISTS.transformArguments(['1', '2']), + ['SCRIPT', 'EXISTS', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.scriptExists', async client => { - assert.deepEqual( - await client.scriptExists('sha1'), - [false] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.scriptExists', async client => { + assert.deepEqual( + await client.scriptExists('sha1'), + [0] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/SCRIPT_EXISTS.ts b/packages/client/lib/commands/SCRIPT_EXISTS.ts index cee889215d3..ab0a293d8de 100644 --- a/packages/client/lib/commands/SCRIPT_EXISTS.ts +++ b/packages/client/lib/commands/SCRIPT_EXISTS.ts @@ -1,8 +1,11 @@ -import { RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { ArrayReply, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export function transformArguments(sha1: string | Array): RedisCommandArguments { - return pushVerdictArguments(['SCRIPT', 'EXISTS'], sha1); -} - -export { transformBooleanArrayReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(sha1: RedisVariadicArgument) { + return pushVariadicArguments(['SCRIPT', 'EXISTS'], sha1); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts b/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts index ae156e937d1..ccc14ecc285 100644 --- a/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts +++ b/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts @@ -1,28 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SCRIPT_FLUSH'; +import SCRIPT_FLUSH from './SCRIPT_FLUSH'; describe('SCRIPT FLUSH', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['SCRIPT', 'FLUSH'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + SCRIPT_FLUSH.transformArguments(), + ['SCRIPT', 'FLUSH'] + ); + }); - it('with mode', () => { - assert.deepEqual( - transformArguments('SYNC'), - ['SCRIPT', 'FLUSH', 'SYNC'] - ); - }); + it('with mode', () => { + assert.deepEqual( + SCRIPT_FLUSH.transformArguments('SYNC'), + ['SCRIPT', 'FLUSH', 'SYNC'] + ); }); + }); - testUtils.testWithClient('client.scriptFlush', async client => { - assert.equal( - await client.scriptFlush(), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.scriptFlush', async client => { + assert.equal( + await client.scriptFlush(), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/SCRIPT_FLUSH.ts b/packages/client/lib/commands/SCRIPT_FLUSH.ts index 2c220e9e3d1..f5426395628 100644 --- a/packages/client/lib/commands/SCRIPT_FLUSH.ts +++ b/packages/client/lib/commands/SCRIPT_FLUSH.ts @@ -1,11 +1,16 @@ -export function transformArguments(mode?: 'ASYNC' | 'SYNC'): Array { +import { SimpleStringReply, Command } from '../RESP/types'; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(mode?: 'ASYNC' | 'SYNC') { const args = ['SCRIPT', 'FLUSH']; if (mode) { - args.push(mode); + args.push(mode); } return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_KILL.spec.ts b/packages/client/lib/commands/SCRIPT_KILL.spec.ts index e57265aa61a..1499c97ac07 100644 --- a/packages/client/lib/commands/SCRIPT_KILL.spec.ts +++ b/packages/client/lib/commands/SCRIPT_KILL.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './SCRIPT_KILL'; +import { strict as assert } from 'node:assert'; +import SCRIPT_KILL from './SCRIPT_KILL'; describe('SCRIPT KILL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['SCRIPT', 'KILL'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SCRIPT_KILL.transformArguments(), + ['SCRIPT', 'KILL'] + ); + }); }); diff --git a/packages/client/lib/commands/SCRIPT_KILL.ts b/packages/client/lib/commands/SCRIPT_KILL.ts index c0a53da8681..ac025b788bb 100644 --- a/packages/client/lib/commands/SCRIPT_KILL.ts +++ b/packages/client/lib/commands/SCRIPT_KILL.ts @@ -1,5 +1,10 @@ -export function transformArguments(): Array { - return ['SCRIPT', 'KILL']; -} +import { SimpleStringReply, Command } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['SCRIPT', 'KILL']; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_LOAD.spec.ts b/packages/client/lib/commands/SCRIPT_LOAD.spec.ts index 062f3c201e1..d964859d2ff 100644 --- a/packages/client/lib/commands/SCRIPT_LOAD.spec.ts +++ b/packages/client/lib/commands/SCRIPT_LOAD.spec.ts @@ -1,23 +1,23 @@ -import { strict as assert } from 'assert'; -import { scriptSha1 } from '../lua-script'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SCRIPT_LOAD'; +import SCRIPT_LOAD from './SCRIPT_LOAD'; +import { scriptSha1 } from '../lua-script'; describe('SCRIPT LOAD', () => { - const SCRIPT = 'return 1;', - SCRIPT_SHA1 = scriptSha1(SCRIPT); + const SCRIPT = 'return 1;', + SCRIPT_SHA1 = scriptSha1(SCRIPT); - it('transformArguments', () => { - assert.deepEqual( - transformArguments(SCRIPT), - ['SCRIPT', 'LOAD', SCRIPT] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SCRIPT_LOAD.transformArguments(SCRIPT), + ['SCRIPT', 'LOAD', SCRIPT] + ); + }); - testUtils.testWithClient('client.scriptLoad', async client => { - assert.equal( - await client.scriptLoad(SCRIPT), - SCRIPT_SHA1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.scriptLoad', async client => { + assert.equal( + await client.scriptLoad(SCRIPT), + SCRIPT_SHA1 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/SCRIPT_LOAD.ts b/packages/client/lib/commands/SCRIPT_LOAD.ts index 7cb28c1ec7f..90028b13a5f 100644 --- a/packages/client/lib/commands/SCRIPT_LOAD.ts +++ b/packages/client/lib/commands/SCRIPT_LOAD.ts @@ -1,5 +1,10 @@ -export function transformArguments(script: string): Array { - return ['SCRIPT', 'LOAD', script]; -} +import { BlobStringReply, Command, RedisArgument } from '../RESP/types'; -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(script: RedisArgument) { + return ['SCRIPT', 'LOAD', script]; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SDIFF.spec.ts b/packages/client/lib/commands/SDIFF.spec.ts index 340906e9350..83ac6dc1da1 100644 --- a/packages/client/lib/commands/SDIFF.spec.ts +++ b/packages/client/lib/commands/SDIFF.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SDIFF'; +import SDIFF from './SDIFF'; describe('SDIFF', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['SDIFF', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + SDIFF.transformArguments('key'), + ['SDIFF', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['SDIFF', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + SDIFF.transformArguments(['1', '2']), + ['SDIFF', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.sDiff', async client => { - assert.deepEqual( - await client.sDiff('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sDiff', async client => { + assert.deepEqual( + await client.sDiff('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SDIFF.ts b/packages/client/lib/commands/SDIFF.ts index 9c4f3b4820b..918cbf7fa15 100644 --- a/packages/client/lib/commands/SDIFF.ts +++ b/packages/client/lib/commands/SDIFF.ts @@ -1,14 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - keys: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['SDIFF'], keys); -} - -export declare function transformReply(): Array; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(keys: RedisVariadicArgument) { + return pushVariadicArguments(['SDIFF'], keys); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SDIFFSTORE.spec.ts b/packages/client/lib/commands/SDIFFSTORE.spec.ts index 263b4f43f64..613a9eb590b 100644 --- a/packages/client/lib/commands/SDIFFSTORE.spec.ts +++ b/packages/client/lib/commands/SDIFFSTORE.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SDIFFSTORE'; +import SDIFFSTORE from './SDIFFSTORE'; describe('SDIFFSTORE', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['SDIFFSTORE', 'destination', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + SDIFFSTORE.transformArguments('destination', 'key'), + ['SDIFFSTORE', 'destination', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['SDIFFSTORE', 'destination', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + SDIFFSTORE.transformArguments('destination', ['1', '2']), + ['SDIFFSTORE', 'destination', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.sDiffStore', async client => { - assert.equal( - await client.sDiffStore('destination', 'key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sDiffStore', async client => { + assert.equal( + await client.sDiffStore('{tag}destination', '{tag}key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SDIFFSTORE.ts b/packages/client/lib/commands/SDIFFSTORE.ts index a927e12ef0e..15f0ccb499a 100644 --- a/packages/client/lib/commands/SDIFFSTORE.ts +++ b/packages/client/lib/commands/SDIFFSTORE.ts @@ -1,13 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - destination: RedisCommandArgument, - keys: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['SDIFFSTORE', destination], keys); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + transformArguments(destination: RedisArgument, keys: RedisVariadicArgument) { + return pushVariadicArguments(['SDIFFSTORE', destination], keys); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SET.spec.ts b/packages/client/lib/commands/SET.spec.ts index 0b3331fd3a4..4364eb7391a 100644 --- a/packages/client/lib/commands/SET.spec.ts +++ b/packages/client/lib/commands/SET.spec.ts @@ -1,129 +1,164 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SET'; +import SET from './SET'; describe('SET', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'value'), - ['SET', 'key', 'value'] - ); - }); + describe('transformArguments', () => { + describe('value', () => { + it('string', () => { + assert.deepEqual( + SET.transformArguments('key', 'value'), + ['SET', 'key', 'value'] + ); + }); + + it('number', () => { + assert.deepEqual( + SET.transformArguments('key', 0), + ['SET', 'key', '0'] + ); + }); + }); + + describe('expiration', () => { + it('\'KEEPTTL\'', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + expiration: 'KEEPTTL' + }), + ['SET', 'key', 'value', 'KEEPTTL'] + ); + }); - it('number', () => { - assert.deepEqual( - transformArguments('key', 0), - ['SET', 'key', '0'] - ); - }); + it('{ type: \'KEEPTTL\' }', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + expiration: { + type: 'KEEPTTL' + } + }), + ['SET', 'key', 'value', 'KEEPTTL'] + ); + }); - describe('TTL', () => { - it('with EX', () => { - assert.deepEqual( - transformArguments('key', 'value', { - EX: 0 - }), - ['SET', 'key', 'value', 'EX', '0'] - ); - }); + it('{ type: \'EX\' }', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + expiration: { + type: 'EX', + value: 0 + } + }), + ['SET', 'key', 'value', 'EX', '0'] + ); + }); - it('with PX', () => { - assert.deepEqual( - transformArguments('key', 'value', { - PX: 0 - }), - ['SET', 'key', 'value', 'PX', '0'] - ); - }); + it('with EX (backwards compatibility)', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + EX: 0 + }), + ['SET', 'key', 'value', 'EX', '0'] + ); + }); - it('with EXAT', () => { - assert.deepEqual( - transformArguments('key', 'value', { - EXAT: 0 - }), - ['SET', 'key', 'value', 'EXAT', '0'] - ); - }); + it('with PX (backwards compatibility)', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + PX: 0 + }), + ['SET', 'key', 'value', 'PX', '0'] + ); + }); - it('with PXAT', () => { - assert.deepEqual( - transformArguments('key', 'value', { - PXAT: 0 - }), - ['SET', 'key', 'value', 'PXAT', '0'] - ); - }); + it('with EXAT (backwards compatibility)', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + EXAT: 0 + }), + ['SET', 'key', 'value', 'EXAT', '0'] + ); + }); - it('with KEEPTTL', () => { - assert.deepEqual( - transformArguments('key', 'value', { - KEEPTTL: true - }), - ['SET', 'key', 'value', 'KEEPTTL'] - ); - }); - }); + it('with PXAT (backwards compatibility)', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + PXAT: 0 + }), + ['SET', 'key', 'value', 'PXAT', '0'] + ); + }); - describe('Guards', () => { - it('with NX', () => { - assert.deepEqual( - transformArguments('key', 'value', { - NX: true - }), - ['SET', 'key', 'value', 'NX'] - ); - }); + it('with KEEPTTL (backwards compatibility)', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + KEEPTTL: true + }), + ['SET', 'key', 'value', 'KEEPTTL'] + ); + }); + }); - it('with XX', () => { - assert.deepEqual( - transformArguments('key', 'value', { - XX: true - }), - ['SET', 'key', 'value', 'XX'] - ); - }); - }); + describe('condition', () => { + it('with condition', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + condition: 'NX' + }), + ['SET', 'key', 'value', 'NX'] + ); + }); - it('with GET', () => { - assert.deepEqual( - transformArguments('key', 'value', { - GET: true - }), - ['SET', 'key', 'value', 'GET'] - ); - }); + it('with NX (backwards compatibility)', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + NX: true + }), + ['SET', 'key', 'value', 'NX'] + ); + }); - it('with EX, NX, GET', () => { - assert.deepEqual( - transformArguments('key', 'value', { - EX: 1, - NX: true, - GET: true - }), - ['SET', 'key', 'value', 'EX', '1', 'NX', 'GET'] - ); - }); + it('with XX (backwards compatibility)', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + XX: true + }), + ['SET', 'key', 'value', 'XX'] + ); + }); }); - describe('client.set', () => { - testUtils.testWithClient('simple', async client => { - assert.equal( - await client.set('key', 'value'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + it('with GET', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + GET: true + }), + ['SET', 'key', 'value', 'GET'] + ); + }); - testUtils.testWithClient('with GET on empty key', async client => { - assert.equal( - await client.set('key', 'value', { - GET: true - }), - null - ); - }, { - ...GLOBAL.SERVERS.OPEN, - minimumDockerVersion: [6, 2] - }); + it('with expiration, condition, GET', () => { + assert.deepEqual( + SET.transformArguments('key', 'value', { + expiration: { + type: 'EX', + value: 0 + }, + condition: 'NX', + GET: true + }), + ['SET', 'key', 'value', 'EX', '0', 'NX', 'GET'] + ); }); + }); + + testUtils.testAll('set', async client => { + assert.equal( + await client.set('key', 'value'), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SET.ts b/packages/client/lib/commands/SET.ts index 08ae56552b9..cede62e7055 100644 --- a/packages/client/lib/commands/SET.ts +++ b/packages/client/lib/commands/SET.ts @@ -1,63 +1,91 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; +export interface SetOptions { + expiration?: { + type: 'EX' | 'PX' | 'EXAT' | 'PXAT'; + value: number; + } | { + type: 'KEEPTTL'; + } | 'KEEPTTL'; + /** + * @deprecated Use `expiration` { type: 'EX', value: number } instead + */ + EX?: number; + /** + * @deprecated Use `expiration` { type: 'PX', value: number } instead + */ + PX?: number; + /** + * @deprecated Use `expiration` { type: 'EXAT', value: number } instead + */ + EXAT?: number; + /** + * @deprecated Use `expiration` { type: 'PXAT', value: number } instead + */ + PXAT?: number; + /** + * @deprecated Use `expiration` 'KEEPTTL' instead + */ + KEEPTTL?: boolean; -type MaximumOneOf = - K extends keyof T ? { [P in K]?: T[K] } & Partial, never>> : never; - -type SetTTL = MaximumOneOf<{ - EX: number; - PX: number; - EXAT: number; - PXAT: number; - KEEPTTL: true; -}>; - -type SetGuards = MaximumOneOf<{ - NX: true; - XX: true; -}>; - -interface SetCommonOptions { - GET?: true; + condition?: 'NX' | 'XX'; + /** + * @deprecated Use `{ condition: 'NX' }` instead. + */ + NX?: boolean; + /** + * @deprecated Use `{ condition: 'XX' }` instead. + */ + XX?: boolean; + + GET?: boolean; } -export type SetOptions = SetTTL & SetGuards & SetCommonOptions; - -export function transformArguments( - key: RedisCommandArgument, - value: RedisCommandArgument | number, - options?: SetOptions -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, value: RedisArgument | number, options?: SetOptions) { const args = [ - 'SET', - key, - typeof value === 'number' ? value.toString() : value + 'SET', + key, + typeof value === 'number' ? value.toString() : value ]; - if (options?.EX !== undefined) { - args.push('EX', options.EX.toString()); + if (options?.expiration) { + if (typeof options.expiration === 'string') { + args.push(options.expiration); + } else if (options.expiration.type === 'KEEPTTL') { + args.push('KEEPTTL'); + } else { + args.push( + options.expiration.type, + options.expiration.value.toString() + ); + } + } else if (options?.EX !== undefined) { + args.push('EX', options.EX.toString()); } else if (options?.PX !== undefined) { - args.push('PX', options.PX.toString()); + args.push('PX', options.PX.toString()); } else if (options?.EXAT !== undefined) { - args.push('EXAT', options.EXAT.toString()); + args.push('EXAT', options.EXAT.toString()); } else if (options?.PXAT !== undefined) { - args.push('PXAT', options.PXAT.toString()); + args.push('PXAT', options.PXAT.toString()); } else if (options?.KEEPTTL) { - args.push('KEEPTTL'); + args.push('KEEPTTL'); } - if (options?.NX) { - args.push('NX'); + if (options?.condition) { + args.push(options.condition); + } else if (options?.NX) { + args.push('NX'); } else if (options?.XX) { - args.push('XX'); + args.push('XX'); } if (options?.GET) { - args.push('GET'); + args.push('GET'); } return args; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SETBIT.spec.ts b/packages/client/lib/commands/SETBIT.spec.ts index 43fbff7c2d9..e4470bb1528 100644 --- a/packages/client/lib/commands/SETBIT.spec.ts +++ b/packages/client/lib/commands/SETBIT.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SETBIT'; +import SETBIT from './SETBIT'; describe('SETBIT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 1), - ['SETBIT', 'key', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SETBIT.transformArguments('key', 0, 1), + ['SETBIT', 'key', '0', '1'] + ); + }); - testUtils.testWithClient('client.setBit', async client => { - assert.equal( - await client.setBit('key', 0, 1), - 0 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.setBit', async cluster => { - assert.equal( - await cluster.setBit('key', 0, 1), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('setBit', async client => { + assert.equal( + await client.setBit('key', 0, 1), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SETBIT.ts b/packages/client/lib/commands/SETBIT.ts index 94f463330a8..5b3ec6173dc 100644 --- a/packages/client/lib/commands/SETBIT.ts +++ b/packages/client/lib/commands/SETBIT.ts @@ -1,14 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { BitValue } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, offset: number, value: BitValue -): RedisCommandArguments { + ) { return ['SETBIT', key, offset.toString(), value.toString()]; -} - -export declare function transformReply(): BitValue; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SETEX.spec.ts b/packages/client/lib/commands/SETEX.spec.ts index bca298c6c04..00f204cc713 100644 --- a/packages/client/lib/commands/SETEX.spec.ts +++ b/packages/client/lib/commands/SETEX.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SETEX'; +import SETEX from './SETEX'; describe('SETEX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1, 'value'), - ['SETEX', 'key', '1', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SETEX.transformArguments('key', 1, 'value'), + ['SETEX', 'key', '1', 'value'] + ); + }); - testUtils.testWithClient('client.setEx', async client => { - assert.equal( - await client.setEx('key', 1, 'value'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.setEx', async cluster => { - assert.equal( - await cluster.setEx('key', 1, 'value'), - 'OK' - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('setEx', async client => { + assert.equal( + await client.setEx('key', 1, 'value'), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SETEX.ts b/packages/client/lib/commands/SETEX.ts index bb3068501f0..bbd77e5d990 100644 --- a/packages/client/lib/commands/SETEX.ts +++ b/packages/client/lib/commands/SETEX.ts @@ -1,18 +1,18 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, seconds: number, - value: RedisCommandArgument -): RedisCommandArguments { + value: RedisArgument + ) { return [ - 'SETEX', - key, - seconds.toString(), - value + 'SETEX', + key, + seconds.toString(), + value ]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/SETNX .spec.ts b/packages/client/lib/commands/SETNX .spec.ts index c5bdfcffa2c..5cfca29ba62 100644 --- a/packages/client/lib/commands/SETNX .spec.ts +++ b/packages/client/lib/commands/SETNX .spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SETNX'; +import SETNX from './SETNX'; describe('SETNX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'value'), - ['SETNX', 'key', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SETNX.transformArguments('key', 'value'), + ['SETNX', 'key', 'value'] + ); + }); - testUtils.testWithClient('client.setNX', async client => { - assert.equal( - await client.setNX('key', 'value'), - true - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.setNX', async cluster => { - assert.equal( - await cluster.setNX('key', 'value'), - true - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('setNX', async client => { + assert.equal( + await client.setNX('key', 'value'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SETNX.ts b/packages/client/lib/commands/SETNX.ts index b01d45dc32f..0940efad693 100644 --- a/packages/client/lib/commands/SETNX.ts +++ b/packages/client/lib/commands/SETNX.ts @@ -1,12 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - value: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + transformArguments(key: RedisArgument, value: RedisArgument) { return ['SETNX', key, value]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SETRANGE.spec.ts b/packages/client/lib/commands/SETRANGE.spec.ts index 398b7730404..01d3545a359 100644 --- a/packages/client/lib/commands/SETRANGE.spec.ts +++ b/packages/client/lib/commands/SETRANGE.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SETRANGE'; +import SETRANGE from './SETRANGE'; describe('SETRANGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 'value'), - ['SETRANGE', 'key', '0', 'value'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SETRANGE.transformArguments('key', 0, 'value'), + ['SETRANGE', 'key', '0', 'value'] + ); + }); - testUtils.testWithClient('client.setRange', async client => { - assert.equal( - await client.setRange('key', 0, 'value'), - 5 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.setRange', async cluster => { - assert.equal( - await cluster.setRange('key', 0, 'value'), - 5 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('setRange', async client => { + assert.equal( + await client.setRange('key', 0, 'value'), + 5 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SETRANGE.ts b/packages/client/lib/commands/SETRANGE.ts index 038a8a5dd7f..1951a82c07d 100644 --- a/packages/client/lib/commands/SETRANGE.ts +++ b/packages/client/lib/commands/SETRANGE.ts @@ -1,13 +1,18 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, offset: number, - value: RedisCommandArgument -): RedisCommandArguments { - return ['SETRANGE', key, offset.toString(), value]; -} - -export declare function transformReply(): number; + value: RedisArgument + ) { + return [ + 'SETRANGE', + key, + offset.toString(), + value + ]; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SHUTDOWN.spec.ts b/packages/client/lib/commands/SHUTDOWN.spec.ts index d58cf4443c7..7dd46a5d534 100644 --- a/packages/client/lib/commands/SHUTDOWN.spec.ts +++ b/packages/client/lib/commands/SHUTDOWN.spec.ts @@ -1,27 +1,49 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './SHUTDOWN'; +import { strict as assert } from 'node:assert'; +import SHUTDOWN from './SHUTDOWN'; describe('SHUTDOWN', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(), - ['SHUTDOWN'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + SHUTDOWN.transformArguments(), + ['SHUTDOWN'] + ); + }); + + it('with mode', () => { + assert.deepEqual( + SHUTDOWN.transformArguments({ + mode: 'NOSAVE' + }), + ['SHUTDOWN', 'NOSAVE'] + ); + }); - it('NOSAVE', () => { - assert.deepEqual( - transformArguments('NOSAVE'), - ['SHUTDOWN', 'NOSAVE'] - ); - }); + it('with NOW', () => { + assert.deepEqual( + SHUTDOWN.transformArguments({ + NOW: true + }), + ['SHUTDOWN', 'NOW'] + ); + }); + + it('with FORCE', () => { + assert.deepEqual( + SHUTDOWN.transformArguments({ + FORCE: true + }), + ['SHUTDOWN', 'FORCE'] + ); + }); - it('SAVE', () => { - assert.deepEqual( - transformArguments('SAVE'), - ['SHUTDOWN', 'SAVE'] - ); - }); + it('with ABORT', () => { + assert.deepEqual( + SHUTDOWN.transformArguments({ + ABORT: true + }), + ['SHUTDOWN', 'ABORT'] + ); }); + }); }); diff --git a/packages/client/lib/commands/SHUTDOWN.ts b/packages/client/lib/commands/SHUTDOWN.ts index 1990d05a2ed..e0f3d08ce81 100644 --- a/packages/client/lib/commands/SHUTDOWN.ts +++ b/packages/client/lib/commands/SHUTDOWN.ts @@ -1,11 +1,35 @@ -export function transformArguments(mode?: 'NOSAVE' | 'SAVE'): Array { - const args = ['SHUTDOWN']; +import { SimpleStringReply, Command } from '../RESP/types'; - if (mode) { - args.push(mode); +export interface ShutdownOptions { + mode?: 'NOSAVE' | 'SAVE'; + NOW?: boolean; + FORCE?: boolean; + ABORT?: boolean; +} + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(options?: ShutdownOptions) { + const args = ['SHUTDOWN'] + + if (options?.mode) { + args.push(options.mode); } - return args; -} + if (options?.NOW) { + args.push('NOW'); + } -export declare function transformReply(): void; + if (options?.FORCE) { + args.push('FORCE'); + } + + if (options?.ABORT) { + args.push('ABORT'); + } + + return args; + }, + transformReply: undefined as unknown as () => void | SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SINTER.spec.ts b/packages/client/lib/commands/SINTER.spec.ts index 2324eac3ee8..5b66fdd3f89 100644 --- a/packages/client/lib/commands/SINTER.spec.ts +++ b/packages/client/lib/commands/SINTER.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SINTER'; +import SINTER from './SINTER'; describe('SINTER', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['SINTER', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + SINTER.transformArguments('key'), + ['SINTER', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['SINTER', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + SINTER.transformArguments(['1', '2']), + ['SINTER', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.sInter', async client => { - assert.deepEqual( - await client.sInter('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sInter', async client => { + assert.deepEqual( + await client.sInter('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SINTER.ts b/packages/client/lib/commands/SINTER.ts index fe1feee7ade..f3f27de2e38 100644 --- a/packages/client/lib/commands/SINTER.ts +++ b/packages/client/lib/commands/SINTER.ts @@ -1,14 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - keys: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['SINTER'], keys); -} - -export declare function transformReply(): Array; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(keys: RedisVariadicArgument) { + return pushVariadicArguments(['SINTER'], keys); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SINTERCARD.spec.ts b/packages/client/lib/commands/SINTERCARD.spec.ts index a93699f6a13..cddb886088a 100644 --- a/packages/client/lib/commands/SINTERCARD.spec.ts +++ b/packages/client/lib/commands/SINTERCARD.spec.ts @@ -1,30 +1,42 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SINTERCARD'; +import SINTERCARD from './SINTERCARD'; describe('SINTERCARD', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['SINTERCARD', '2', '1', '2'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + SINTERCARD.transformArguments(['1', '2']), + ['SINTERCARD', '2', '1', '2'] + ); + }); + + it('with limit (backwards compatibility)', () => { + assert.deepEqual( + SINTERCARD.transformArguments(['1', '2'], 1), + ['SINTERCARD', '2', '1', '2', 'LIMIT', '1'] + ); + }); - it('with limit', () => { - assert.deepEqual( - transformArguments(['1', '2'], 1), - ['SINTERCARD', '2', '1', '2', 'LIMIT', '1'] - ); - }); + it('with LIMIT', () => { + assert.deepEqual( + SINTERCARD.transformArguments(['1', '2'], { + LIMIT: 1 + }), + ['SINTERCARD', '2', '1', '2', 'LIMIT', '1'] + ); }); + }); - testUtils.testWithClient('client.sInterCard', async client => { - assert.deepEqual( - await client.sInterCard('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sInterCard', async client => { + assert.deepEqual( + await client.sInterCard('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SINTERCARD.ts b/packages/client/lib/commands/SINTERCARD.ts index ddb7e5b00ef..626bc1048c3 100644 --- a/packages/client/lib/commands/SINTERCARD.ts +++ b/packages/client/lib/commands/SINTERCARD.ts @@ -1,21 +1,26 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; +export interface SInterCardOptions { + LIMIT?: number; +} -export function transformArguments( - keys: Array | RedisCommandArgument, - limit?: number -): RedisCommandArguments { - const args = pushVerdictArgument(['SINTERCARD'], keys); +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments( + keys: RedisVariadicArgument, + options?: SInterCardOptions | number // `number` for backwards compatibility + ) { + const args = pushVariadicArgument(['SINTERCARD'], keys); - if (limit) { - args.push('LIMIT', limit.toString()); + if (typeof options === 'number') { // backwards compatibility + args.push('LIMIT', options.toString()); + } else if (options?.LIMIT !== undefined) { + args.push('LIMIT', options.LIMIT.toString()); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SINTERSTORE.spec.ts b/packages/client/lib/commands/SINTERSTORE.spec.ts index c4a6a095e7d..05416742ee9 100644 --- a/packages/client/lib/commands/SINTERSTORE.spec.ts +++ b/packages/client/lib/commands/SINTERSTORE.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SINTERSTORE'; +import SINTERSTORE from './SINTERSTORE'; describe('SINTERSTORE', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['SINTERSTORE', 'destination', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + SINTERSTORE.transformArguments('destination', 'key'), + ['SINTERSTORE', 'destination', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['SINTERSTORE', 'destination', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + SINTERSTORE.transformArguments('destination', ['1', '2']), + ['SINTERSTORE', 'destination', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.sInterStore', async client => { - assert.equal( - await client.sInterStore('destination', 'key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sInterStore', async client => { + assert.equal( + await client.sInterStore('{tag}destination', '{tag}key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SINTERSTORE.ts b/packages/client/lib/commands/SINTERSTORE.ts index 02bf9d061a0..744e0b18456 100644 --- a/packages/client/lib/commands/SINTERSTORE.ts +++ b/packages/client/lib/commands/SINTERSTORE.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - destination: RedisCommandArgument, - keys: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['SINTERSTORE', destination], keys); -} - -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + destination: RedisArgument, + keys: RedisVariadicArgument + ) { + return pushVariadicArguments(['SINTERSTORE', destination], keys); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SISMEMBER.spec.ts b/packages/client/lib/commands/SISMEMBER.spec.ts index 8d18c83697a..0c1b92614cb 100644 --- a/packages/client/lib/commands/SISMEMBER.spec.ts +++ b/packages/client/lib/commands/SISMEMBER.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SISMEMBER'; +import SISMEMBER from './SISMEMBER'; describe('SISMEMBER', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['SISMEMBER', 'key', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SISMEMBER.transformArguments('key', 'member'), + ['SISMEMBER', 'key', 'member'] + ); + }); - testUtils.testWithClient('client.sIsMember', async client => { - assert.equal( - await client.sIsMember('key', 'member'), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sIsMember', async client => { + assert.equal( + await client.sIsMember('key', 'member'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SISMEMBER.ts b/packages/client/lib/commands/SISMEMBER.ts index 4d40c63250e..0687d19de30 100644 --- a/packages/client/lib/commands/SISMEMBER.ts +++ b/packages/client/lib/commands/SISMEMBER.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, member: RedisArgument) { return ['SISMEMBER', key, member]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SMEMBERS.spec.ts b/packages/client/lib/commands/SMEMBERS.spec.ts index b9c58c9eebb..016b01ff747 100644 --- a/packages/client/lib/commands/SMEMBERS.spec.ts +++ b/packages/client/lib/commands/SMEMBERS.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SMEMBERS'; +import SMEMBERS from './SMEMBERS'; describe('SMEMBERS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['SMEMBERS', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SMEMBERS.transformArguments('key'), + ['SMEMBERS', 'key'] + ); + }); - testUtils.testWithClient('client.sMembers', async client => { - assert.deepEqual( - await client.sMembers('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sMembers', async client => { + assert.deepEqual( + await client.sMembers('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SMEMBERS.ts b/packages/client/lib/commands/SMEMBERS.ts index 7950a4c073a..391c83af6c6 100644 --- a/packages/client/lib/commands/SMEMBERS.ts +++ b/packages/client/lib/commands/SMEMBERS.ts @@ -1,9 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, BlobStringReply, SetReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['SMEMBERS', key]; -} - -export declare function transformReply(): Array; + }, + transformReply: { + 2: undefined as unknown as () => ArrayReply, + 3: undefined as unknown as () => SetReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/SMISMEMBER.spec.ts b/packages/client/lib/commands/SMISMEMBER.spec.ts index e3728134029..273ab05dd75 100644 --- a/packages/client/lib/commands/SMISMEMBER.spec.ts +++ b/packages/client/lib/commands/SMISMEMBER.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SMISMEMBER'; +import SMISMEMBER from './SMISMEMBER'; describe('SMISMEMBER', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['SMISMEMBER', 'key', '1', '2'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SMISMEMBER.transformArguments('key', ['1', '2']), + ['SMISMEMBER', 'key', '1', '2'] + ); + }); - testUtils.testWithClient('client.smIsMember', async client => { - assert.deepEqual( - await client.smIsMember('key', ['1', '2']), - [false, false] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('smIsMember', async client => { + assert.deepEqual( + await client.smIsMember('key', ['1', '2']), + [0, 0] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SMISMEMBER.ts b/packages/client/lib/commands/SMISMEMBER.ts index 175120bdfb9..bdf48d45ab4 100644 --- a/packages/client/lib/commands/SMISMEMBER.ts +++ b/packages/client/lib/commands/SMISMEMBER.ts @@ -1,12 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - members: Array -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, members: Array) { return ['SMISMEMBER', key, ...members]; -} - -export { transformBooleanArrayReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SMOVE.spec.ts b/packages/client/lib/commands/SMOVE.spec.ts index e3308ee8143..7ff2f773a7b 100644 --- a/packages/client/lib/commands/SMOVE.spec.ts +++ b/packages/client/lib/commands/SMOVE.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SMOVE'; +import SMOVE from './SMOVE'; describe('SMOVE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination', 'member'), - ['SMOVE', 'source', 'destination', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SMOVE.transformArguments('source', 'destination', 'member'), + ['SMOVE', 'source', 'destination', 'member'] + ); + }); - testUtils.testWithClient('client.sMove', async client => { - assert.equal( - await client.sMove('source', 'destination', 'member'), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sMove', async client => { + assert.equal( + await client.sMove('{tag}source', '{tag}destination', 'member'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SMOVE.ts b/packages/client/lib/commands/SMOVE.ts index 83c4027dbd5..183b363fb90 100644 --- a/packages/client/lib/commands/SMOVE.ts +++ b/packages/client/lib/commands/SMOVE.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: RedisCommandArgument, - destination: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + source: RedisArgument, + destination: RedisArgument, + member: RedisArgument + ) { return ['SMOVE', source, destination, member]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SORT.spec.ts b/packages/client/lib/commands/SORT.spec.ts index 4967b020ad5..4fce8113755 100644 --- a/packages/client/lib/commands/SORT.spec.ts +++ b/packages/client/lib/commands/SORT.spec.ts @@ -1,96 +1,99 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SORT'; +import SORT from './SORT'; describe('SORT', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key'), - ['SORT', 'key'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + SORT.transformArguments('key'), + ['SORT', 'key'] + ); + }); - it('with BY', () => { - assert.deepEqual( - transformArguments('key', { - BY: 'pattern' - }), - ['SORT', 'key', 'BY', 'pattern'] - ); - }); + it('with BY', () => { + assert.deepEqual( + SORT.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'] - ); - }); + it('with LIMIT', () => { + assert.deepEqual( + SORT.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'] - ); - }); + describe('with GET', () => { + it('string', () => { + assert.deepEqual( + SORT.transformArguments('key', { + GET: 'pattern' + }), + ['SORT', 'key', 'GET', 'pattern'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', { - GET: ['1', '2'] - }), - ['SORT', 'key', 'GET', '1', 'GET', '2'] - ); - }); - }); + it('array', () => { + assert.deepEqual( + SORT.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 DIRECTION', () => { + assert.deepEqual( + SORT.transformArguments('key', { + DIRECTION: 'ASC' + }), + ['SORT', 'key', 'ASC'] + ); + }); - it('with ALPHA', () => { - assert.deepEqual( - transformArguments('key', { - ALPHA: true - }), - ['SORT', 'key', 'ALPHA'] - ); - }); + it('with ALPHA', () => { + assert.deepEqual( + SORT.transformArguments('key', { + ALPHA: true + }), + ['SORT', 'key', 'ALPHA'] + ); + }); - it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => { - assert.deepEqual( - transformArguments('key', { - BY: 'pattern', - LIMIT: { - offset: 0, - count: 1 - }, - GET: 'pattern', - DIRECTION: 'ASC', - ALPHA: true - }), - ['SORT', 'key', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA'] - ); - }); + it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => { + assert.deepEqual( + SORT.transformArguments('key', { + BY: 'pattern', + LIMIT: { + offset: 0, + count: 1 + }, + GET: 'pattern', + DIRECTION: 'ASC', + ALPHA: true + }), + ['SORT', 'key', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA'] + ); }); + }); - testUtils.testWithClient('client.sort', async client => { - assert.deepEqual( - await client.sort('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sort', async client => { + assert.deepEqual( + await client.sort('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SORT.ts b/packages/client/lib/commands/SORT.ts index 15e95bde677..b71383943e9 100644 --- a/packages/client/lib/commands/SORT.ts +++ b/packages/client/lib/commands/SORT.ts @@ -1,13 +1,59 @@ -import { RedisCommandArguments } from '.'; -import { pushSortArguments, SortOptions } from './generic-transformers'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; +export interface SortOptions { + BY?: RedisArgument; + LIMIT?: { + offset: number; + count: number; + }; + GET?: RedisArgument | Array; + DIRECTION?: 'ASC' | 'DESC'; + ALPHA?: boolean; +} + +export function transformSortArguments( + command: RedisArgument, + key: RedisArgument, + options?: SortOptions +) { + const args: Array = [command, 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) { + if (Array.isArray(options.GET)) { + for (const pattern of options.GET) { + args.push('GET', pattern); + } + } else { + args.push('GET', options.GET); + } + } + + if (options?.DIRECTION) { + args.push(options.DIRECTION); + } + + if (options?.ALPHA) { + args.push('ALPHA'); + } -export function transformArguments( - key: string, - options?: SortOptions -): RedisCommandArguments { - return pushSortArguments(['SORT', key], options); + return args; } -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments: transformSortArguments.bind(undefined, 'SORT'), + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SORT_RO.spec.ts b/packages/client/lib/commands/SORT_RO.spec.ts index fe3ca1240d7..963416ae639 100644 --- a/packages/client/lib/commands/SORT_RO.spec.ts +++ b/packages/client/lib/commands/SORT_RO.spec.ts @@ -1,98 +1,101 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SORT_RO'; +import SORT_RO from './SORT_RO'; describe('SORT_RO', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key'), - ['SORT_RO', 'key'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + SORT_RO.transformArguments('key'), + ['SORT_RO', 'key'] + ); + }); - it('with BY', () => { - assert.deepEqual( - transformArguments('key', { - BY: 'pattern' - }), - ['SORT_RO', 'key', 'BY', 'pattern'] - ); - }); + it('with BY', () => { + assert.deepEqual( + SORT_RO.transformArguments('key', { + BY: 'pattern' + }), + ['SORT_RO', 'key', 'BY', 'pattern'] + ); + }); - it('with LIMIT', () => { - assert.deepEqual( - transformArguments('key', { - LIMIT: { - offset: 0, - count: 1 - } - }), - ['SORT_RO', 'key', 'LIMIT', '0', '1'] - ); - }); + it('with LIMIT', () => { + assert.deepEqual( + SORT_RO.transformArguments('key', { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['SORT_RO', 'key', 'LIMIT', '0', '1'] + ); + }); - describe('with GET', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', { - GET: 'pattern' - }), - ['SORT_RO', 'key', 'GET', 'pattern'] - ); - }); + describe('with GET', () => { + it('string', () => { + assert.deepEqual( + SORT_RO.transformArguments('key', { + GET: 'pattern' + }), + ['SORT_RO', 'key', 'GET', 'pattern'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', { - GET: ['1', '2'] - }), - ['SORT_RO', 'key', 'GET', '1', 'GET', '2'] - ); - }); - }); + it('array', () => { + assert.deepEqual( + SORT_RO.transformArguments('key', { + GET: ['1', '2'] + }), + ['SORT_RO', 'key', 'GET', '1', 'GET', '2'] + ); + }); + }); - it('with DIRECTION', () => { - assert.deepEqual( - transformArguments('key', { - DIRECTION: 'ASC' - }), - ['SORT_RO', 'key', 'ASC'] - ); - }); + it('with DIRECTION', () => { + assert.deepEqual( + SORT_RO.transformArguments('key', { + DIRECTION: 'ASC' + }), + ['SORT_RO', 'key', 'ASC'] + ); + }); - it('with ALPHA', () => { - assert.deepEqual( - transformArguments('key', { - ALPHA: true - }), - ['SORT_RO', 'key', 'ALPHA'] - ); - }); + it('with ALPHA', () => { + assert.deepEqual( + SORT_RO.transformArguments('key', { + ALPHA: true + }), + ['SORT_RO', 'key', 'ALPHA'] + ); + }); - it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => { - assert.deepEqual( - transformArguments('key', { - BY: 'pattern', - LIMIT: { - offset: 0, - count: 1 - }, - GET: 'pattern', - DIRECTION: 'ASC', - ALPHA: true, - }), - ['SORT_RO', 'key', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA'] - ); - }); + it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => { + assert.deepEqual( + SORT_RO.transformArguments('key', { + BY: 'pattern', + LIMIT: { + offset: 0, + count: 1 + }, + GET: 'pattern', + DIRECTION: 'ASC', + ALPHA: true, + }), + ['SORT_RO', 'key', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA'] + ); }); + }); - testUtils.testWithClient('client.sortRo', async client => { - assert.deepEqual( - await client.sortRo('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sortRo', async client => { + assert.deepEqual( + await client.sortRo('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SORT_RO.ts b/packages/client/lib/commands/SORT_RO.ts index 4af7acd80d7..459a0bbc03d 100644 --- a/packages/client/lib/commands/SORT_RO.ts +++ b/packages/client/lib/commands/SORT_RO.ts @@ -1,15 +1,9 @@ -import { RedisCommandArguments } from '.'; -import { pushSortArguments, SortOptions } from "./generic-transformers"; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: string, - options?: SortOptions -): RedisCommandArguments { - return pushSortArguments(['SORT_RO', key], options); -} - -export declare function transformReply(): Array; +import { Command } from '../RESP/types'; +import SORT, { transformSortArguments } from './SORT'; + +export default { + FIRST_KEY_INDEX: SORT.FIRST_KEY_INDEX, + IS_READ_ONLY: true, + transformArguments: transformSortArguments.bind(undefined, 'SORT_RO'), + transformReply: SORT.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SORT_STORE.spec.ts b/packages/client/lib/commands/SORT_STORE.spec.ts index d078135255d..49efd4c6ad8 100644 --- a/packages/client/lib/commands/SORT_STORE.spec.ts +++ b/packages/client/lib/commands/SORT_STORE.spec.ts @@ -1,96 +1,99 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SORT_STORE'; +import SORT_STORE from './SORT_STORE'; describe('SORT STORE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('source', 'destination'), - ['SORT', 'source', 'STORE', 'destination'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + SORT_STORE.transformArguments('source', 'destination'), + ['SORT', 'source', 'STORE', 'destination'] + ); + }); - it('with BY', () => { - assert.deepEqual( - transformArguments('source', 'destination', { - BY: 'pattern' - }), - ['SORT', 'source', 'BY', 'pattern', 'STORE', 'destination'] - ); - }); + it('with BY', () => { + assert.deepEqual( + SORT_STORE.transformArguments('source', 'destination', { + BY: 'pattern' + }), + ['SORT', 'source', 'BY', 'pattern', 'STORE', 'destination'] + ); + }); - it('with LIMIT', () => { - assert.deepEqual( - transformArguments('source', 'destination', { - LIMIT: { - offset: 0, - count: 1 - } - }), - ['SORT', 'source', 'LIMIT', '0', '1', 'STORE', 'destination'] - ); - }); + it('with LIMIT', () => { + assert.deepEqual( + SORT_STORE.transformArguments('source', 'destination', { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['SORT', 'source', 'LIMIT', '0', '1', 'STORE', 'destination'] + ); + }); - describe('with GET', () => { - it('string', () => { - assert.deepEqual( - transformArguments('source', 'destination', { - GET: 'pattern' - }), - ['SORT', 'source', 'GET', 'pattern', 'STORE', 'destination'] - ); - }); + describe('with GET', () => { + it('string', () => { + assert.deepEqual( + SORT_STORE.transformArguments('source', 'destination', { + GET: 'pattern' + }), + ['SORT', 'source', 'GET', 'pattern', 'STORE', 'destination'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('source', 'destination', { - GET: ['1', '2'] - }), - ['SORT', 'source', 'GET', '1', 'GET', '2', 'STORE', 'destination'] - ); - }); - }); + it('array', () => { + assert.deepEqual( + SORT_STORE.transformArguments('source', 'destination', { + GET: ['1', '2'] + }), + ['SORT', 'source', 'GET', '1', 'GET', '2', 'STORE', 'destination'] + ); + }); + }); - it('with DIRECTION', () => { - assert.deepEqual( - transformArguments('source', 'destination', { - DIRECTION: 'ASC' - }), - ['SORT', 'source', 'ASC', 'STORE', 'destination'] - ); - }); + it('with DIRECTION', () => { + assert.deepEqual( + SORT_STORE.transformArguments('source', 'destination', { + DIRECTION: 'ASC' + }), + ['SORT', 'source', 'ASC', 'STORE', 'destination'] + ); + }); - it('with ALPHA', () => { - assert.deepEqual( - transformArguments('source', 'destination', { - ALPHA: true - }), - ['SORT', 'source', 'ALPHA', 'STORE', 'destination'] - ); - }); + it('with ALPHA', () => { + assert.deepEqual( + SORT_STORE.transformArguments('source', 'destination', { + ALPHA: true + }), + ['SORT', 'source', 'ALPHA', 'STORE', 'destination'] + ); + }); - it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => { - assert.deepEqual( - transformArguments('source', 'destination', { - BY: 'pattern', - LIMIT: { - offset: 0, - count: 1 - }, - GET: 'pattern', - DIRECTION: 'ASC', - ALPHA: true - }), - ['SORT', 'source', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA', 'STORE', 'destination'] - ); - }); + it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => { + assert.deepEqual( + SORT_STORE.transformArguments('source', 'destination', { + BY: 'pattern', + LIMIT: { + offset: 0, + count: 1 + }, + GET: 'pattern', + DIRECTION: 'ASC', + ALPHA: true + }), + ['SORT', 'source', 'BY', 'pattern', 'LIMIT', '0', '1', 'GET', 'pattern', 'ASC', 'ALPHA', 'STORE', 'destination'] + ); }); + }); - testUtils.testWithClient('client.sortStore', async client => { - assert.equal( - await client.sortStore('source', 'destination'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sortStore', async client => { + assert.equal( + await client.sortStore('{tag}source', '{tag}destination'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SORT_STORE.ts b/packages/client/lib/commands/SORT_STORE.ts index 9acaf023175..b6ad709fb60 100644 --- a/packages/client/lib/commands/SORT_STORE.ts +++ b/packages/client/lib/commands/SORT_STORE.ts @@ -1,17 +1,17 @@ -import { RedisCommandArguments } from '.'; -import { SortOptions } from './generic-transformers'; -import { transformArguments as transformSortArguments } from './SORT'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import SORT, { SortOptions } from './SORT'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - source: string, - destination: string, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + source: RedisArgument, + destination: RedisArgument, options?: SortOptions -): RedisCommandArguments { - const args = transformSortArguments(source, options); + ) { + const args = SORT.transformArguments(source, options); args.push('STORE', destination); return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SPOP.spec.ts b/packages/client/lib/commands/SPOP.spec.ts index 6a384d181fc..896c1c820ac 100644 --- a/packages/client/lib/commands/SPOP.spec.ts +++ b/packages/client/lib/commands/SPOP.spec.ts @@ -1,28 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SPOP'; +import SPOP from './SPOP'; describe('SPOP', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key'), - ['SPOP', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SPOP.transformArguments('key'), + ['SPOP', 'key'] + ); + }); - it('with count', () => { - assert.deepEqual( - transformArguments('key', 2), - ['SPOP', 'key', '2'] - ); - }); - }); - - testUtils.testWithClient('client.sPop', async client => { - assert.equal( - await client.sPop('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sPop', async client => { + assert.equal( + await client.sPop('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SPOP.ts b/packages/client/lib/commands/SPOP.ts index 38ce8573f3f..4b061a86306 100644 --- a/packages/client/lib/commands/SPOP.ts +++ b/packages/client/lib/commands/SPOP.ts @@ -1,18 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - count?: number -): RedisCommandArguments { - const args = ['SPOP', key]; - - if (typeof count === 'number') { - args.push(count.toString()); - } - - return args; -} - -export declare function transformReply(): Array; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument) { + return ['SPOP', key]; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SPOP_COUNT.spec.ts b/packages/client/lib/commands/SPOP_COUNT.spec.ts new file mode 100644 index 00000000000..ddad816b420 --- /dev/null +++ b/packages/client/lib/commands/SPOP_COUNT.spec.ts @@ -0,0 +1,22 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import SPOP_COUNT from './SPOP_COUNT'; + +describe('SPOP_COUNT', () => { + it('transformArguments', () => { + assert.deepEqual( + SPOP_COUNT.transformArguments('key', 1), + ['SPOP', 'key', '1'] + ); + }); + + testUtils.testAll('sPopCount', async client => { + assert.deepEqual( + await client.sPopCount('key', 1), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/SPOP_COUNT.ts b/packages/client/lib/commands/SPOP_COUNT.ts new file mode 100644 index 00000000000..4c68ae8d08e --- /dev/null +++ b/packages/client/lib/commands/SPOP_COUNT.ts @@ -0,0 +1,10 @@ +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, count: number) { + return ['SPOP', key, count.toString()]; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SPUBLISH.spec.ts b/packages/client/lib/commands/SPUBLISH.spec.ts index 60b6ce2dad0..741372d0154 100644 --- a/packages/client/lib/commands/SPUBLISH.spec.ts +++ b/packages/client/lib/commands/SPUBLISH.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SPUBLISH'; +import SPUBLISH from './SPUBLISH'; describe('SPUBLISH', () => { - testUtils.isVersionGreaterThanHook([7]); - - it('transformArguments', () => { - assert.deepEqual( - transformArguments('channel', 'message'), - ['SPUBLISH', 'channel', 'message'] - ); - }); + testUtils.isVersionGreaterThanHook([7]); - testUtils.testWithClient('client.sPublish', async client => { - assert.equal( - await client.sPublish('channel', 'message'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + it('transformArguments', () => { + assert.deepEqual( + SPUBLISH.transformArguments('channel', 'message'), + ['SPUBLISH', 'channel', 'message'] + ); + }); + + testUtils.testAll('sPublish', async client => { + assert.equal( + await client.sPublish('channel', 'message'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SPUBLISH.ts b/packages/client/lib/commands/SPUBLISH.ts index 42a7ab49072..19d84b03c6f 100644 --- a/packages/client/lib/commands/SPUBLISH.ts +++ b/packages/client/lib/commands/SPUBLISH.ts @@ -1,14 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const IS_READ_ONLY = true; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - channel: RedisCommandArgument, - message: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(channel: RedisArgument, message: RedisArgument) { return ['SPUBLISH', channel, message]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SRANDMEMBER.spec.ts b/packages/client/lib/commands/SRANDMEMBER.spec.ts index 291271540be..a7df01f0eb9 100644 --- a/packages/client/lib/commands/SRANDMEMBER.spec.ts +++ b/packages/client/lib/commands/SRANDMEMBER.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SRANDMEMBER'; +import SRANDMEMBER from './SRANDMEMBER'; describe('SRANDMEMBER', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['SRANDMEMBER', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SRANDMEMBER.transformArguments('key'), + ['SRANDMEMBER', 'key'] + ); + }); - testUtils.testWithClient('client.sRandMember', async client => { - assert.equal( - await client.sRandMember('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sRandMember', async client => { + assert.equal( + await client.sRandMember('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SRANDMEMBER.ts b/packages/client/lib/commands/SRANDMEMBER.ts index d84e61993e5..6a2373ae927 100644 --- a/packages/client/lib/commands/SRANDMEMBER.ts +++ b/packages/client/lib/commands/SRANDMEMBER.ts @@ -1,9 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['SRANDMEMBER', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts b/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts index d3d787b3e63..364eb640341 100644 --- a/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts +++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SRANDMEMBER_COUNT'; +import SRANDMEMBER_COUNT from './SRANDMEMBER_COUNT'; describe('SRANDMEMBER COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['SRANDMEMBER', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SRANDMEMBER_COUNT.transformArguments('key', 1), + ['SRANDMEMBER', 'key', '1'] + ); + }); - testUtils.testWithClient('client.sRandMemberCount', async client => { - assert.deepEqual( - await client.sRandMemberCount('key', 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sRandMemberCount', async client => { + assert.deepEqual( + await client.sRandMemberCount('key', 1), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts index d265d89e9a6..778f3d8f629 100644 --- a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts @@ -1,16 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformSRandMemberArguments } from './SRANDMEMBER'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import SRANDMEMBER from './SRANDMEMBER'; -export { FIRST_KEY_INDEX } from './SRANDMEMBER'; - -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { - return [ - ...transformSRandMemberArguments(key), - count.toString() - ]; -} - -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: SRANDMEMBER.FIRST_KEY_INDEX, + IS_READ_ONLY: SRANDMEMBER.IS_READ_ONLY, + transformArguments(key: RedisArgument, count: number) { + const args = SRANDMEMBER.transformArguments(key); + args.push(count.toString()); + return args; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SREM.spec.ts b/packages/client/lib/commands/SREM.spec.ts index d53d7b0334d..f17c6fb118e 100644 --- a/packages/client/lib/commands/SREM.spec.ts +++ b/packages/client/lib/commands/SREM.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SREM'; +import SREM from './SREM'; describe('SREM', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['SREM', 'key', 'member'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + SREM.transformArguments('key', 'member'), + ['SREM', 'key', 'member'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['SREM', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + SREM.transformArguments('key', ['1', '2']), + ['SREM', 'key', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.sRem', async client => { - assert.equal( - await client.sRem('key', 'member'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sRem', async client => { + assert.equal( + await client.sRem('key', 'member'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SREM.ts b/packages/client/lib/commands/SREM.ts index 34aebdf02e3..daa95493d02 100644 --- a/packages/client/lib/commands/SREM.ts +++ b/packages/client/lib/commands/SREM.ts @@ -1,13 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - members: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['SREM', key], members); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, members: RedisVariadicArgument) { + return pushVariadicArguments(['SREM', key], members); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SSCAN.spec.ts b/packages/client/lib/commands/SSCAN.spec.ts index 71a90bf81d8..29a13306fde 100644 --- a/packages/client/lib/commands/SSCAN.spec.ts +++ b/packages/client/lib/commands/SSCAN.spec.ts @@ -1,74 +1,55 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './SSCAN'; +import SSCAN 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'] - ); - }); + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + SSCAN.transformArguments('key', '0'), + ['SSCAN', 'key', '0'] + ); + }); - it('with MATCH & COUNT', () => { - assert.deepEqual( - transformArguments('key', 0, { - MATCH: 'pattern', - COUNT: 1 - }), - ['SSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] - ); - }); + it('with MATCH', () => { + assert.deepEqual( + SSCAN.transformArguments('key', '0', { + MATCH: 'pattern' + }), + ['SSCAN', 'key', '0', 'MATCH', 'pattern'] + ); }); - describe('transformReply', () => { - it('without members', () => { - assert.deepEqual( - transformReply(['0', []]), - { - cursor: 0, - members: [] - } - ); - }); + it('with COUNT', () => { + assert.deepEqual( + SSCAN.transformArguments('key', '0', { + COUNT: 1 + }), + ['SSCAN', 'key', '0', 'COUNT', '1'] + ); + }); - it('with members', () => { - assert.deepEqual( - transformReply(['0', ['member']]), - { - cursor: 0, - members: ['member'] - } - ); - }); + it('with MATCH & COUNT', () => { + assert.deepEqual( + SSCAN.transformArguments('key', '0', { + MATCH: 'pattern', + COUNT: 1 + }), + ['SSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] + ); }); + }); - testUtils.testWithClient('client.sScan', async client => { - assert.deepEqual( - await client.sScan('key', 0), - { - cursor: 0, - members: [] - } - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sScan', async client => { + assert.deepEqual( + await client.sScan('key', '0'), + { + cursor: '0', + members: [] + } + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SSCAN.ts b/packages/client/lib/commands/SSCAN.ts index 9b3938f159b..f47144d834c 100644 --- a/packages/client/lib/commands/SSCAN.ts +++ b/packages/client/lib/commands/SSCAN.ts @@ -1,31 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions, pushScanArguments } from './generic-transformers'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; +import { ScanCommonOptions, pushScanArguments } from './SCAN'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - cursor: number, - options?: ScanOptions -): RedisCommandArguments { - return pushScanArguments([ - 'SSCAN', - key, - ], cursor, options); -} - -type SScanRawReply = [string, Array]; - -interface SScanReply { - cursor: number; - members: Array; -} - -export function transformReply([cursor, members]: SScanRawReply): SScanReply { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + cursor: RedisArgument, + options?: ScanCommonOptions + ) { + return pushScanArguments(['SSCAN', key], cursor, options); + }, + transformReply([cursor, members]: [BlobStringReply, Array]) { return { - cursor: Number(cursor), - members + cursor, + members }; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/STRLEN.spec.ts b/packages/client/lib/commands/STRLEN.spec.ts index 519c68d3e5d..b07c07b909a 100644 --- a/packages/client/lib/commands/STRLEN.spec.ts +++ b/packages/client/lib/commands/STRLEN.spec.ts @@ -1,26 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './STRLEN'; +import STRLEN from './STRLEN'; describe('STRLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['STRLEN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + STRLEN.transformArguments('key'), + ['STRLEN', 'key'] + ); + }); - testUtils.testWithClient('client.strLen', async client => { - assert.equal( - await client.strLen('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithCluster('cluster.strLen', async cluster => { - assert.equal( - await cluster.strLen('key'), - 0 - ); - }, GLOBAL.CLUSTERS.OPEN); + testUtils.testAll('strLen', async client => { + assert.equal( + await client.strLen('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/STRLEN.ts b/packages/client/lib/commands/STRLEN.ts index de88340d8b6..594530ff6bf 100644 --- a/packages/client/lib/commands/STRLEN.ts +++ b/packages/client/lib/commands/STRLEN.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['STRLEN', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SUNION.spec.ts b/packages/client/lib/commands/SUNION.spec.ts index 2918607c1d6..ff00c44a1b1 100644 --- a/packages/client/lib/commands/SUNION.spec.ts +++ b/packages/client/lib/commands/SUNION.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUNION'; +import SUNION from './SUNION'; describe('SUNION', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['SUNION', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + SUNION.transformArguments('key'), + ['SUNION', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['SUNION', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + SUNION.transformArguments(['1', '2']), + ['SUNION', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.sUnion', async client => { - assert.deepEqual( - await client.sUnion('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sUnion', async client => { + assert.deepEqual( + await client.sUnion('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SUNION.ts b/packages/client/lib/commands/SUNION.ts index 52c112e6610..42042217e2b 100644 --- a/packages/client/lib/commands/SUNION.ts +++ b/packages/client/lib/commands/SUNION.ts @@ -1,14 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - keys: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['SUNION'], keys); -} - -export declare function transformReply(): Array; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(keys: RedisVariadicArgument) { + return pushVariadicArguments(['SUNION'], keys); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SUNIONSTORE.spec.ts b/packages/client/lib/commands/SUNIONSTORE.spec.ts index 142533eea2b..790fd78060a 100644 --- a/packages/client/lib/commands/SUNIONSTORE.spec.ts +++ b/packages/client/lib/commands/SUNIONSTORE.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUNIONSTORE'; +import SUNIONSTORE from './SUNIONSTORE'; describe('SUNIONSTORE', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['SUNIONSTORE', 'destination', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + SUNIONSTORE.transformArguments('destination', 'key'), + ['SUNIONSTORE', 'destination', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['SUNIONSTORE', 'destination', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + SUNIONSTORE.transformArguments('destination', ['1', '2']), + ['SUNIONSTORE', 'destination', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.sUnionStore', async client => { - assert.equal( - await client.sUnionStore('destination', 'key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('sUnionStore', async client => { + assert.equal( + await client.sUnionStore('{tag}destination', '{tag}key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/SUNIONSTORE.ts b/packages/client/lib/commands/SUNIONSTORE.ts index 94df6771a04..9adaa9288f3 100644 --- a/packages/client/lib/commands/SUNIONSTORE.ts +++ b/packages/client/lib/commands/SUNIONSTORE.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - destination: RedisCommandArgument, - keys: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['SUNIONSTORE', destination], keys); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + destination: RedisArgument, + keys: RedisVariadicArgument + ) { + return pushVariadicArguments(['SUNIONSTORE', destination], keys); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/SWAPDB.spec.ts b/packages/client/lib/commands/SWAPDB.spec.ts index add87512a64..9331854c13b 100644 --- a/packages/client/lib/commands/SWAPDB.spec.ts +++ b/packages/client/lib/commands/SWAPDB.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SWAPDB'; +import SWAPDB from './SWAPDB'; describe('SWAPDB', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0, 1), - ['SWAPDB', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + SWAPDB.transformArguments(0, 1), + ['SWAPDB', '0', '1'] + ); + }); - testUtils.testWithClient('client.swapDb', async client => { - assert.equal( - await client.swapDb(0, 1), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.swapDb', async client => { + assert.equal( + await client.swapDb(0, 1), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/SWAPDB.ts b/packages/client/lib/commands/SWAPDB.ts index 7f13d6b008e..f3b768e95f4 100644 --- a/packages/client/lib/commands/SWAPDB.ts +++ b/packages/client/lib/commands/SWAPDB.ts @@ -1,5 +1,11 @@ -export function transformArguments(index1: number, index2: number): Array { +import { SimpleStringReply, Command } from '../RESP/types'; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(index1: number, index2: number) { return ['SWAPDB', index1.toString(), index2.toString()]; -} + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; -export declare function transformReply(): string; diff --git a/packages/client/lib/commands/TIME.spec.ts b/packages/client/lib/commands/TIME.spec.ts index bbaa7942db0..d9dd9667ea4 100644 --- a/packages/client/lib/commands/TIME.spec.ts +++ b/packages/client/lib/commands/TIME.spec.ts @@ -1,18 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './TIME'; +import TIME from './TIME'; describe('TIME', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['TIME'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + TIME.transformArguments(), + ['TIME'] + ); + }); - testUtils.testWithClient('client.time', async client => { - const reply = await client.time(); - assert.ok(reply instanceof Date); - assert.ok(typeof reply.microseconds === 'number'); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.time', async client => { + const reply = await client.time(); + assert.ok(Array.isArray(reply)); + assert.equal(typeof reply[0], 'string'); + assert.equal(typeof reply[1], 'string'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/TIME.ts b/packages/client/lib/commands/TIME.ts index 1a364d6d8be..d4dc67ae483 100644 --- a/packages/client/lib/commands/TIME.ts +++ b/packages/client/lib/commands/TIME.ts @@ -1,15 +1,13 @@ -export function transformArguments(): Array { - return ['TIME']; -} - -interface TimeReply extends Date { - microseconds: number; -} +import { BlobStringReply, Command } from '../RESP/types'; -export function transformReply(reply: [string, string]): TimeReply { - const seconds = Number(reply[0]), - microseconds = Number(reply[1]), - d: Partial = new Date(seconds * 1000 + microseconds / 1000); - d.microseconds = microseconds; - return d as TimeReply; -} +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['TIME']; + }, + transformReply: undefined as unknown as () => [ + unixTimestamp: BlobStringReply<`${number}`>, + microseconds: BlobStringReply<`${number}`> + ] +} as const satisfies Command; diff --git a/packages/client/lib/commands/TOUCH.spec.ts b/packages/client/lib/commands/TOUCH.spec.ts index 578c49587d7..48e77900ee3 100644 --- a/packages/client/lib/commands/TOUCH.spec.ts +++ b/packages/client/lib/commands/TOUCH.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './TOUCH'; +import TOUCH from './TOUCH'; describe('TOUCH', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['TOUCH', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + TOUCH.transformArguments('key'), + ['TOUCH', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['TOUCH', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + TOUCH.transformArguments(['1', '2']), + ['TOUCH', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.touch', async client => { - assert.equal( - await client.touch('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('touch', async client => { + assert.equal( + await client.touch('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/TOUCH.ts b/packages/client/lib/commands/TOUCH.ts index e67dff8e932..c1c19402f8b 100644 --- a/packages/client/lib/commands/TOUCH.ts +++ b/packages/client/lib/commands/TOUCH.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['TOUCH'], key); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisVariadicArgument) { + return pushVariadicArguments(['TOUCH'], key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/TTL.spec.ts b/packages/client/lib/commands/TTL.spec.ts index e37a6ab714b..6b709226a2b 100644 --- a/packages/client/lib/commands/TTL.spec.ts +++ b/packages/client/lib/commands/TTL.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './TTL'; +import TTL from './TTL'; describe('TTL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TTL', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + TTL.transformArguments('key'), + ['TTL', 'key'] + ); + }); - testUtils.testWithClient('client.ttl', async client => { - assert.equal( - await client.ttl('key'), - -2 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('ttl', async client => { + assert.equal( + await client.ttl('key'), + -2 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/TTL.ts b/packages/client/lib/commands/TTL.ts index 29586f31fa8..65c3b7b026f 100644 --- a/packages/client/lib/commands/TTL.ts +++ b/packages/client/lib/commands/TTL.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['TTL', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/TYPE.spec.ts b/packages/client/lib/commands/TYPE.spec.ts index 1040bf979b3..45cf1cfc1c9 100644 --- a/packages/client/lib/commands/TYPE.spec.ts +++ b/packages/client/lib/commands/TYPE.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './TYPE'; +import TYPE from './TYPE'; describe('TYPE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['TYPE', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + TYPE.transformArguments('key'), + ['TYPE', 'key'] + ); + }); - testUtils.testWithClient('client.type', async client => { - assert.equal( - await client.type('key'), - 'none' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('type', async client => { + assert.equal( + await client.type('key'), + 'none' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/TYPE.ts b/packages/client/lib/commands/TYPE.ts index 10cd3f99b0e..09f6887492c 100644 --- a/packages/client/lib/commands/TYPE.ts +++ b/packages/client/lib/commands/TYPE.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['TYPE', key]; -} - -export declare function transformReply(): RedisCommandArgument; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/UNLINK.spec.ts b/packages/client/lib/commands/UNLINK.spec.ts index e8355407d8f..1e374783007 100644 --- a/packages/client/lib/commands/UNLINK.spec.ts +++ b/packages/client/lib/commands/UNLINK.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './UNLINK'; +import UNLINK from './UNLINK'; describe('UNLINK', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['UNLINK', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + UNLINK.transformArguments('key'), + ['UNLINK', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['UNLINK', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + UNLINK.transformArguments(['1', '2']), + ['UNLINK', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.unlink', async client => { - assert.equal( - await client.unlink('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('unlink', async client => { + assert.equal( + await client.unlink('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/UNLINK.ts b/packages/client/lib/commands/UNLINK.ts index 53b0360e2df..2346573f397 100644 --- a/packages/client/lib/commands/UNLINK.ts +++ b/packages/client/lib/commands/UNLINK.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['UNLINK'], key); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisVariadicArgument) { + return pushVariadicArguments(['UNLINK'], key); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/UNWATCH.spec.ts b/packages/client/lib/commands/UNWATCH.spec.ts deleted file mode 100644 index 109ed0fa7c0..00000000000 --- a/packages/client/lib/commands/UNWATCH.spec.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { strict as assert } from 'assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './UNWATCH'; - -describe('UNWATCH', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['UNWATCH'] - ); - }); - - testUtils.testWithClient('client.unwatch', async client => { - assert.equal( - await client.unwatch(), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/client/lib/commands/UNWATCH.ts b/packages/client/lib/commands/UNWATCH.ts deleted file mode 100644 index ce42e7697bf..00000000000 --- a/packages/client/lib/commands/UNWATCH.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function transformArguments(): Array { - return ['UNWATCH']; -} - -export declare function transformReply(): string; diff --git a/packages/client/lib/commands/WAIT.spec.ts b/packages/client/lib/commands/WAIT.spec.ts index c85ef598612..61b197c90ba 100644 --- a/packages/client/lib/commands/WAIT.spec.ts +++ b/packages/client/lib/commands/WAIT.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './WAIT'; +import WAIT from './WAIT'; describe('WAIT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(0, 1), - ['WAIT', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + WAIT.transformArguments(0, 1), + ['WAIT', '0', '1'] + ); + }); - testUtils.testWithClient('client.wait', async client => { - assert.equal( - await client.wait(0, 1), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.wait', async client => { + assert.equal( + await client.wait(0, 1), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/WAIT.ts b/packages/client/lib/commands/WAIT.ts index dff51ed9680..21c39a643e5 100644 --- a/packages/client/lib/commands/WAIT.ts +++ b/packages/client/lib/commands/WAIT.ts @@ -1,7 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { NumberReply, Command } from '../RESP/types'; -export function transformArguments(numberOfReplicas: number, timeout: number): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(numberOfReplicas: number, timeout: number) { return ['WAIT', numberOfReplicas.toString(), timeout.toString()]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/WATCH.spec.ts b/packages/client/lib/commands/WATCH.spec.ts deleted file mode 100644 index acaa062874f..00000000000 --- a/packages/client/lib/commands/WATCH.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -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/packages/client/lib/commands/WATCH.ts b/packages/client/lib/commands/WATCH.ts deleted file mode 100644 index 58c6dfd1dad..00000000000 --- a/packages/client/lib/commands/WATCH.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: string | Array): RedisCommandArguments { - return pushVerdictArguments(['WATCH'], key); -} - -export declare function transformReply(): string; diff --git a/packages/client/lib/commands/XACK.spec.ts b/packages/client/lib/commands/XACK.spec.ts index 0586a5921fd..81a7954ffd6 100644 --- a/packages/client/lib/commands/XACK.spec.ts +++ b/packages/client/lib/commands/XACK.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XACK'; +import XACK from './XACK'; describe('XACK', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'group', '1-0'), - ['XACK', 'key', 'group', '1-0'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + XACK.transformArguments('key', 'group', '0-0'), + ['XACK', 'key', 'group', '0-0'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', 'group', ['1-0', '2-0']), - ['XACK', 'key', 'group', '1-0', '2-0'] - ); - }); + it('array', () => { + assert.deepEqual( + XACK.transformArguments('key', 'group', ['0-0', '1-0']), + ['XACK', 'key', 'group', '0-0', '1-0'] + ); }); + }); - testUtils.testWithClient('client.xAck', async client => { - assert.equal( - await client.xAck('key', 'group', '1-0'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xAck', async client => { + assert.equal( + await client.xAck('key', 'group', '0-0'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XACK.ts b/packages/client/lib/commands/XACK.ts index 670d810fc00..89b2d581201 100644 --- a/packages/client/lib/commands/XACK.ts +++ b/packages/client/lib/commands/XACK.ts @@ -1,14 +1,15 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - id: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['XACK', key, group], id); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + group: RedisArgument, + id: RedisVariadicArgument + ) { + return pushVariadicArguments(['XACK', key, group], id); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XADD.spec.ts b/packages/client/lib/commands/XADD.spec.ts index 4b556ecc27c..10c6f4daa56 100644 --- a/packages/client/lib/commands/XADD.spec.ts +++ b/packages/client/lib/commands/XADD.spec.ts @@ -1,118 +1,93 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XADD'; +import XADD 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'] - ); - }); + describe('transformArguments', () => { + it('single field', () => { + assert.deepEqual( + XADD.transformArguments('key', '*', { + field: 'value' + }), + ['XADD', 'key', '*', 'field', 'value'] + ); + }); - it('with TRIM', () => { - assert.deepEqual( - transformArguments('key', '*', { - field: 'value' - }, { - TRIM: { - threshold: 1000 - } - }), - ['XADD', 'key', '1000', '*', 'field', 'value'] - ); - }); + it('multiple fields', () => { + assert.deepEqual( + XADD.transformArguments('key', '*', { + '1': 'I', + '2': 'II' + }), + ['XADD', 'key', '*', '1', 'I', '2', 'II'] + ); + }); - it('with TRIM.strategy', () => { - assert.deepEqual( - transformArguments('key', '*', { - field: 'value' - }, { - TRIM: { - strategy: 'MAXLEN', - threshold: 1000 - } - }), - ['XADD', 'key', 'MAXLEN', '1000', '*','field', 'value'] - ); - }); + it('with TRIM', () => { + assert.deepEqual( + XADD.transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000 + } + }), + ['XADD', 'key', '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.strategy', () => { + assert.deepEqual( + XADD.transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + strategy: 'MAXLEN', + threshold: 1000 + } + }), + ['XADD', 'key', 'MAXLEN', '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 TRIM.strategyModifier', () => { + assert.deepEqual( + XADD.transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + strategyModifier: '=', + threshold: 1000 + } + }), + ['XADD', 'key', '=', '1000', '*', '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'] - ); - }); + it('with TRIM.limit', () => { + assert.deepEqual( + XADD.transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000, + limit: 1 + } + }), + ['XADD', 'key', '1000', 'LIMIT', '1', '*', 'field', 'value'] + ); }); + }); - testUtils.testWithClient('client.xAdd', async client => { - assert.equal( - typeof await client.xAdd('key', '*', { - field: 'value' - }), - 'string' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xAdd', async client => { + assert.equal( + typeof await client.xAdd('key', '*', { + field: 'value' + }), + 'string' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XADD.ts b/packages/client/lib/commands/XADD.ts index e7a1b6804ff..b681069c729 100644 --- a/packages/client/lib/commands/XADD.ts +++ b/packages/client/lib/commands/XADD.ts @@ -1,52 +1,55 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -interface XAddOptions { - NOMKSTREAM?: true; - TRIM?: { - strategy?: 'MAXLEN' | 'MINID'; - strategyModifier?: '=' | '~'; - threshold: number; - limit?: number; - }; +import { RedisArgument, BlobStringReply, Command, CommandArguments } from '../RESP/types'; + +export interface XAddOptions { + TRIM?: { + strategy?: 'MAXLEN' | 'MINID'; + strategyModifier?: '=' | '~'; + threshold: number; + limit?: number; + }; } -export function transformArguments( - key: RedisCommandArgument, - id: RedisCommandArgument, - message: Record, - options?: XAddOptions -): RedisCommandArguments { - const args = ['XADD', key]; - - if (options?.NOMKSTREAM) { - args.push('NOMKSTREAM'); +export function pushXAddArguments( + args: CommandArguments, + id: RedisArgument, + message: Record, + options?: XAddOptions +) { + if (options?.TRIM) { + if (options.TRIM.strategy) { + args.push(options.TRIM.strategy); } - if (options?.TRIM) { - if (options.TRIM.strategy) { - args.push(options.TRIM.strategy); - } - - if (options.TRIM.strategyModifier) { - args.push(options.TRIM.strategyModifier); - } + if (options.TRIM.strategyModifier) { + args.push(options.TRIM.strategyModifier); + } - args.push(options.TRIM.threshold.toString()); + args.push(options.TRIM.threshold.toString()); - if (options.TRIM.limit) { - args.push('LIMIT', options.TRIM.limit.toString()); - } + if (options.TRIM.limit) { + args.push('LIMIT', options.TRIM.limit.toString()); } + } - args.push(id); + args.push(id); - for (const [key, value] of Object.entries(message)) { - args.push(key, value); - } + for (const [key, value] of Object.entries(message)) { + args.push(key, value); + } - return args; + return args; } -export declare function transformReply(): string; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + id: RedisArgument, + message: Record, + options?: XAddOptions + ) { + return pushXAddArguments(['XADD', key], id, message, options); + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts new file mode 100644 index 00000000000..a3dd5602a3b --- /dev/null +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts @@ -0,0 +1,95 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import XADD_NOMKSTREAM from './XADD_NOMKSTREAM'; + +describe('XADD NOMKSTREAM', () => { + testUtils.isVersionGreaterThanHook([6, 2]); + + describe('transformArguments', () => { + it('single field', () => { + assert.deepEqual( + XADD_NOMKSTREAM.transformArguments('key', '*', { + field: 'value' + }), + ['XADD', 'key', 'NOMKSTREAM', '*', 'field', 'value'] + ); + }); + + it('multiple fields', () => { + assert.deepEqual( + XADD_NOMKSTREAM.transformArguments('key', '*', { + '1': 'I', + '2': 'II' + }), + ['XADD', 'key', 'NOMKSTREAM', '*', '1', 'I', '2', 'II'] + ); + }); + + it('with TRIM', () => { + assert.deepEqual( + XADD_NOMKSTREAM.transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000 + } + }), + ['XADD', 'key', 'NOMKSTREAM', '1000', '*', 'field', 'value'] + ); + }); + + it('with TRIM.strategy', () => { + assert.deepEqual( + XADD_NOMKSTREAM.transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + strategy: 'MAXLEN', + threshold: 1000 + } + }), + ['XADD', 'key', 'NOMKSTREAM', 'MAXLEN', '1000', '*', 'field', 'value'] + ); + }); + + it('with TRIM.strategyModifier', () => { + assert.deepEqual( + XADD_NOMKSTREAM.transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + strategyModifier: '=', + threshold: 1000 + } + }), + ['XADD', 'key', 'NOMKSTREAM', '=', '1000', '*', 'field', 'value'] + ); + }); + + it('with TRIM.limit', () => { + assert.deepEqual( + XADD_NOMKSTREAM.transformArguments('key', '*', { + field: 'value' + }, { + TRIM: { + threshold: 1000, + limit: 1 + } + }), + ['XADD', 'key', 'NOMKSTREAM', '1000', 'LIMIT', '1', '*', 'field', 'value'] + ); + }); + }); + + testUtils.testAll('xAddNoMkStream', async client => { + assert.equal( + await client.xAddNoMkStream('key', '*', { + field: 'value' + }), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.ts new file mode 100644 index 00000000000..65e7dd566e3 --- /dev/null +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.ts @@ -0,0 +1,16 @@ +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; +import { XAddOptions, pushXAddArguments } from './XADD'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + id: RedisArgument, + message: Record, + options?: XAddOptions + ) { + return pushXAddArguments(['XADD', key, 'NOMKSTREAM'], id, message, options); + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XAUTOCLAIM.spec.ts b/packages/client/lib/commands/XAUTOCLAIM.spec.ts index bae914bda05..256c58cc4d6 100644 --- a/packages/client/lib/commands/XAUTOCLAIM.spec.ts +++ b/packages/client/lib/commands/XAUTOCLAIM.spec.ts @@ -1,98 +1,68 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XAUTOCLAIM'; +import XAUTOCLAIM from './XAUTOCLAIM'; describe('XAUTOCLAIM', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([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'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + XAUTOCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0'), + ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0'] + ); }); - testUtils.testWithClient('client.xAutoClaim without messages', async client => { - const [,, reply] = await Promise.all([ - client.xGroupCreate('key', 'group', '$', { MKSTREAM: true }), - client.xGroupCreateConsumer('key', 'group', 'consumer'), - client.xAutoClaim('key', 'group', 'consumer', 1, '0-0') - ]); - - assert.deepEqual(reply, { - nextId: '0-0', - messages: [] - }); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('client.xAutoClaim with messages', async client => { - const [,, id,, reply] = await Promise.all([ - client.xGroupCreate('key', 'group', '$', { MKSTREAM: true }), - client.xGroupCreateConsumer('key', 'group', 'consumer'), - client.xAdd('key', '*', { foo: 'bar' }), - client.xReadGroup('group', 'consumer', { key: 'key', id: '>' }), - client.xAutoClaim('key', 'group', 'consumer', 0, '0-0') - ]); + it('with COUNT', () => { + assert.deepEqual( + XAUTOCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + COUNT: 1 + }), + ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'COUNT', '1'] + ); + }); + }); - assert.deepEqual(reply, { - nextId: '0-0', - messages: [{ - id, - message: Object.create(null, { - foo: { - value: 'bar', - configurable: true, - enumerable: true - } - }) - }] - }); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xAutoClaim', async client => { + const message = Object.create(null, { + field: { + value: 'value', + enumerable: true + } + }); - testUtils.testWithClient('client.xAutoClaim with trimmed messages', async client => { - const [,,,,, id,, reply] = await Promise.all([ - client.xGroupCreate('key', 'group', '$', { MKSTREAM: true }), - client.xGroupCreateConsumer('key', 'group', 'consumer'), - client.xAdd('key', '*', { foo: 'bar' }), - client.xReadGroup('group', 'consumer', { key: 'key', id: '>' }), - client.xTrim('key', 'MAXLEN', 0), - client.xAdd('key', '*', { bar: 'baz' }), - client.xReadGroup('group', 'consumer', { key: 'key', id: '>' }), - client.xAutoClaim('key', 'group', 'consumer', 0, '0-0') - ]); + const [, id1, id2, , , reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xAdd('key', '*', message), + client.xAdd('key', '*', message), + client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }), + client.xTrim('key', 'MAXLEN', 1), + client.xAutoClaim('key', 'group', 'consumer', 0, '0-0') + ]); - assert.deepEqual(reply, { - nextId: '0-0', - messages: testUtils.isVersionGreaterThan([7, 0]) ? [{ - id, - message: Object.create(null, { - bar: { - value: 'baz', - configurable: true, - enumerable: true - } - }) - }] : [null, { - id, - message: Object.create(null, { - bar: { - value: 'baz', - configurable: true, - enumerable: true - } - }) - }] - }); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, { + nextId: '0-0', + ...(testUtils.isVersionGreaterThan([7, 0]) ? { + messages: [{ + id: id2, + message + }], + deletedMessages: [id1] + } : { + messages: [null, { + id: id2, + message + }], + deletedMessages: undefined + }) + }); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XAUTOCLAIM.ts b/packages/client/lib/commands/XAUTOCLAIM.ts index 831563981a6..7d33142de32 100644 --- a/packages/client/lib/commands/XAUTOCLAIM.ts +++ b/packages/client/lib/commands/XAUTOCLAIM.ts @@ -1,39 +1,47 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { StreamMessagesNullReply, transformStreamMessagesNullReply } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, TuplesReply, BlobStringReply, ArrayReply, NullReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { StreamMessageRawReply, transformStreamMessageNullReply } from './generic-transformers'; export interface XAutoClaimOptions { - COUNT?: number; + COUNT?: number; } -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - consumer: RedisCommandArgument, +export type XAutoClaimRawReply = TuplesReply<[ + nextId: BlobStringReply, + messages: ArrayReply, + deletedMessages: ArrayReply +]>; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + group: RedisArgument, + consumer: RedisArgument, minIdleTime: number, - start: string, + start: RedisArgument, options?: XAutoClaimOptions -): RedisCommandArguments { - const args = ['XAUTOCLAIM', key, group, consumer, minIdleTime.toString(), start]; + ) { + const args = [ + 'XAUTOCLAIM', + key, + group, + consumer, + minIdleTime.toString(), + start + ]; if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + args.push('COUNT', options.COUNT.toString()); } return args; -} - -type XAutoClaimRawReply = [RedisCommandArgument, Array]; - -interface XAutoClaimReply { - nextId: RedisCommandArgument; - messages: StreamMessagesNullReply; -} - -export function transformReply(reply: XAutoClaimRawReply): XAutoClaimReply { + }, + transformReply(reply: UnwrapReply, preserve?: any, typeMapping?: TypeMapping) { return { - nextId: reply[0], - messages: transformStreamMessagesNullReply(reply[1]) + nextId: reply[0], + messages: (reply[1] as unknown as UnwrapReply).map(transformStreamMessageNullReply.bind(undefined, typeMapping)), + deletedMessages: reply[2] }; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts index 9aa24cd04a4..96ceb1d8116 100644 --- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts +++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts @@ -1,31 +1,37 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XAUTOCLAIM_JUSTID'; +import XAUTOCLAIM_JUSTID from './XAUTOCLAIM_JUSTID'; describe('XAUTOCLAIM JUSTID', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group', 'consumer', 1, '0-0'), - ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XAUTOCLAIM_JUSTID.transformArguments('key', 'group', 'consumer', 1, '0-0'), + ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID'] + ); + }); - testUtils.testWithClient('client.xAutoClaimJustId', async client => { - await Promise.all([ - client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }), - client.xGroupCreateConsumer('key', 'group', 'consumer'), - ]); + testUtils.testWithClient('client.xAutoClaimJustId', async client => { + const [, , id, , reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xGroupCreateConsumer('key', 'group', 'consumer'), + client.xAdd('key', '*', { + field: 'value' + }), + client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }), + client.xAutoClaimJustId('key', 'group', 'consumer', 0, '0-0') + ]); - assert.deepEqual( - await client.xAutoClaimJustId('key', 'group', 'consumer', 1, '0-0'), - { - nextId: '0-0', - messages: [] - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, { + nextId: '0-0', + messages: [id], + deletedMessages: testUtils.isVersionGreaterThan([7, 0]) ? [] : undefined + }); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts index a30ac1579e7..e2832f23536 100644 --- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts @@ -1,25 +1,25 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformXAutoClaimArguments } from './XAUTOCLAIM'; +import { TuplesReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '../RESP/types'; +import XAUTOCLAIM from './XAUTOCLAIM'; -export { FIRST_KEY_INDEX } from './XAUTOCLAIM'; +type XAutoClaimJustIdRawReply = TuplesReply<[ + nextId: BlobStringReply, + messages: ArrayReply, + deletedMessages: ArrayReply +]>; -export function transformArguments(...args: Parameters): RedisCommandArguments { - return [ - ...transformXAutoClaimArguments(...args), - 'JUSTID' - ]; -} - -type XAutoClaimJustIdRawReply = [RedisCommandArgument, Array]; - -interface XAutoClaimJustIdReply { - nextId: RedisCommandArgument; - messages: Array; -} - -export function transformReply(reply: XAutoClaimJustIdRawReply): XAutoClaimJustIdReply { +export default { + FIRST_KEY_INDEX: XAUTOCLAIM.FIRST_KEY_INDEX, + IS_READ_ONLY: XAUTOCLAIM.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = XAUTOCLAIM.transformArguments(...args); + redisArgs.push('JUSTID'); + return redisArgs; + }, + transformReply(reply: UnwrapReply) { return { - nextId: reply[0], - messages: reply[1] + nextId: reply[0], + messages: reply[1], + deletedMessages: reply[2] }; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/XCLAIM.spec.ts b/packages/client/lib/commands/XCLAIM.spec.ts index 6626e84c731..e8fde2e1a1a 100644 --- a/packages/client/lib/commands/XCLAIM.spec.ts +++ b/packages/client/lib/commands/XCLAIM.spec.ts @@ -1,120 +1,125 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XCLAIM'; +import XCLAIM 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()] - ); - }); + describe('transformArguments', () => { + it('single id (string)', () => { + assert.deepEqual( + XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0'), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0'] + ); + }); - it('with RETRYCOUNT', () => { - assert.deepEqual( - transformArguments('key', 'group', 'consumer', 1, '0-0', { - RETRYCOUNT: 1 - }), - ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'RETRYCOUNT', '1'] - ); - }); + it('multiple ids (array)', () => { + assert.deepEqual( + XCLAIM.transformArguments('key', 'group', 'consumer', 1, ['0-0', '1-0']), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', '1-0'] + ); + }); - 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', () => { + assert.deepEqual( + XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + IDLE: 1 + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1'] + ); + }); + + describe('with TIME', () => { + it('number', () => { + assert.deepEqual( + XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + TIME: 1 + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', '1'] + ); + }); + + it('Date', () => { + const d = new Date(); + assert.deepEqual( + XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + TIME: d + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', d.getTime().toString()] + ); + }); + }); - 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'] - ); - }); + it('with RETRYCOUNT', () => { + assert.deepEqual( + XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + RETRYCOUNT: 1 + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'RETRYCOUNT', '1'] + ); }); - testUtils.testWithClient('client.xClaim', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); + it('with FORCE', () => { + assert.deepEqual( + XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + FORCE: true + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'FORCE'] + ); + }); - assert.deepEqual( - await client.xClaim('key', 'group', 'consumer', 0, '0-0'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + it('with LASTID', () => { + assert.deepEqual( + XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + LASTID: '0-0' + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'LASTID', '0-0'] + ); + }); - testUtils.testWithClient('client.xClaim with a message', async client => { - await client.xGroupCreate('key', 'group', '$', { MKSTREAM: true }); - const id = await client.xAdd('key', '*', { foo: 'bar' }); - await client.xReadGroup('group', 'consumer', { key: 'key', id: '>' }); + it('with IDLE, TIME, RETRYCOUNT, FORCE, LASTID', () => { + assert.deepEqual( + XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + IDLE: 1, + TIME: 1, + RETRYCOUNT: 1, + FORCE: true, + LASTID: '0-0' + }), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1', 'TIME', '1', 'RETRYCOUNT', '1', 'FORCE', 'LASTID', '0-0'] + ); + }); + }); - assert.deepEqual( - await client.xClaim('key', 'group', 'consumer', 0, id), - [{ - id, - message: Object.create(null, { 'foo': { - value: 'bar', - configurable: true, - enumerable: true - } }) - }] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xClaim', async client => { + const message = Object.create(null, { + field: { + value: 'value', + enumerable: true + } + }); - testUtils.testWithClient('client.xClaim with a trimmed message', async client => { - await client.xGroupCreate('key', 'group', '$', { MKSTREAM: true }); - const id = await client.xAdd('key', '*', { foo: 'bar' }); - await client.xReadGroup('group', 'consumer', { key: 'key', id: '>' }); - await client.xTrim('key', 'MAXLEN', 0); + const [, , , , , reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xAdd('key', '1-0', message), + client.xAdd('key', '2-0', message), + client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }), + client.xTrim('key', 'MAXLEN', 1), + client.xClaim('key', 'group', 'consumer', 0, ['1-0', '2-0']) + ]); - assert.deepEqual( - await client.xClaim('key', 'group', 'consumer', 0, id), - testUtils.isVersionGreaterThan([7, 0]) ? []: [null] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [ + ...(testUtils.isVersionGreaterThan([7, 0]) ? [] : [null]), + { + id: '2-0', + message + } + ]); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XCLAIM.ts b/packages/client/lib/commands/XCLAIM.ts index e7b458e2376..eb9c0b325e1 100644 --- a/packages/client/lib/commands/XCLAIM.ts +++ b/packages/client/lib/commands/XCLAIM.ts @@ -1,48 +1,60 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, NullReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments, StreamMessageRawReply, transformStreamMessageNullReply } from './generic-transformers'; export interface XClaimOptions { - IDLE?: number; - TIME?: number | Date; - RETRYCOUNT?: number; - FORCE?: true; + IDLE?: number; + TIME?: number | Date; + RETRYCOUNT?: number; + FORCE?: boolean; + LASTID?: RedisArgument; } -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - consumer: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + group: RedisArgument, + consumer: RedisArgument, minIdleTime: number, - id: RedisCommandArgument | Array, + id: RedisVariadicArgument, options?: XClaimOptions -): RedisCommandArguments { - const args = pushVerdictArguments( - ['XCLAIM', key, group, consumer, minIdleTime.toString()], - id + ) { + const args = pushVariadicArguments( + ['XCLAIM', key, group, consumer, minIdleTime.toString()], + id ); - if (options?.IDLE) { - args.push('IDLE', options.IDLE.toString()); + if (options?.IDLE !== undefined) { + args.push('IDLE', options.IDLE.toString()); } - if (options?.TIME) { - args.push( - 'TIME', - (typeof options.TIME === 'number' ? options.TIME : options.TIME.getTime()).toString() - ); + if (options?.TIME !== undefined) { + args.push( + 'TIME', + (options.TIME instanceof Date ? options.TIME.getTime() : options.TIME).toString() + ); } - if (options?.RETRYCOUNT) { - args.push('RETRYCOUNT', options.RETRYCOUNT.toString()); + if (options?.RETRYCOUNT !== undefined) { + args.push('RETRYCOUNT', options.RETRYCOUNT.toString()); } if (options?.FORCE) { - args.push('FORCE'); + args.push('FORCE'); } - return args; -} + if (options?.LASTID !== undefined) { + args.push('LASTID', options.LASTID); + } -export { transformStreamMessagesNullReply as transformReply } from './generic-transformers'; + return args; + }, + transformReply( + reply: UnwrapReply>, + preserve?: any, + typeMapping?: TypeMapping + ) { + return reply.map(transformStreamMessageNullReply.bind(undefined, typeMapping)); + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts b/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts index 619f876d53d..6b580ac3c1b 100644 --- a/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts +++ b/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts @@ -1,23 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XCLAIM_JUSTID'; +import XCLAIM_JUSTID 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'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XCLAIM_JUSTID.transformArguments('key', 'group', 'consumer', 1, '0-0'), + ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID'] + ); + }); - testUtils.testWithClient('client.xClaimJustId', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); + // TODO: test with messages + testUtils.testWithClient('client.xClaimJustId', async client => { + const [, reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xClaimJustId('key', 'group', 'consumer', 1, '0-0') + ]); - assert.deepEqual( - await client.xClaimJustId('key', 'group', 'consumer', 1, '0-0'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, []); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/XCLAIM_JUSTID.ts b/packages/client/lib/commands/XCLAIM_JUSTID.ts index 50d0d5a0366..6200c9106e1 100644 --- a/packages/client/lib/commands/XCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XCLAIM_JUSTID.ts @@ -1,13 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformXClaimArguments } from './XCLAIM'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import XCLAIM from './XCLAIM'; -export { FIRST_KEY_INDEX } from './XCLAIM'; - -export function transformArguments(...args: Parameters): RedisCommandArguments { - return [ - ...transformXClaimArguments(...args), - 'JUSTID' - ]; -} - -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: XCLAIM.FIRST_KEY_INDEX, + IS_READ_ONLY: XCLAIM.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = XCLAIM.transformArguments(...args); + redisArgs.push('JUSTID'); + return redisArgs; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XDEL.spec.ts b/packages/client/lib/commands/XDEL.spec.ts index 00f9e2f9c67..15875d3b7b3 100644 --- a/packages/client/lib/commands/XDEL.spec.ts +++ b/packages/client/lib/commands/XDEL.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XDEL'; +import XDEL from './XDEL'; describe('XDEL', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', '0-0'), - ['XDEL', 'key', '0-0'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + XDEL.transformArguments('key', '0-0'), + ['XDEL', 'key', '0-0'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['0-0', '1-0']), - ['XDEL', 'key', '0-0', '1-0'] - ); - }); + it('array', () => { + assert.deepEqual( + XDEL.transformArguments('key', ['0-0', '1-0']), + ['XDEL', 'key', '0-0', '1-0'] + ); }); + }); - testUtils.testWithClient('client.xDel', async client => { - assert.equal( - await client.xDel('key', '0-0'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xDel', async client => { + assert.equal( + await client.xDel('key', '0-0'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XDEL.ts b/packages/client/lib/commands/XDEL.ts index 82b30d21092..acc9198c2b8 100644 --- a/packages/client/lib/commands/XDEL.ts +++ b/packages/client/lib/commands/XDEL.ts @@ -1,13 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - id: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['XDEL', key], id); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, id: RedisVariadicArgument) { + return pushVariadicArguments(['XDEL', key], id); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XGROUP_CREATE.spec.ts b/packages/client/lib/commands/XGROUP_CREATE.spec.ts index 57516e44cc8..5c9071289c9 100644 --- a/packages/client/lib/commands/XGROUP_CREATE.spec.ts +++ b/packages/client/lib/commands/XGROUP_CREATE.spec.ts @@ -1,32 +1,44 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XGROUP_CREATE'; +import XGROUP_CREATE from './XGROUP_CREATE'; describe('XGROUP CREATE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 'group', '$'), - ['XGROUP', 'CREATE', 'key', 'group', '$'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + XGROUP_CREATE.transformArguments('key', 'group', '$'), + ['XGROUP', 'CREATE', 'key', 'group', '$'] + ); + }); + + it('with MKSTREAM', () => { + assert.deepEqual( + XGROUP_CREATE.transformArguments('key', 'group', '$', { + MKSTREAM: true + }), + ['XGROUP', 'CREATE', 'key', 'group', '$', 'MKSTREAM'] + ); + }); - it('with MKSTREAM', () => { - assert.deepEqual( - transformArguments('key', 'group', '$', { - MKSTREAM: true - }), - ['XGROUP', 'CREATE', 'key', 'group', '$', 'MKSTREAM'] - ); - }); + it('with ENTRIESREAD', () => { + assert.deepEqual( + XGROUP_CREATE.transformArguments('key', 'group', '$', { + ENTRIESREAD: 1 + }), + ['XGROUP', 'CREATE', 'key', 'group', '$', 'ENTRIESREAD', '1'] + ); }); + }); - testUtils.testWithClient('client.xGroupCreate', async client => { - assert.equal( - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xGroupCreate', async client => { + assert.equal( + await client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XGROUP_CREATE.ts b/packages/client/lib/commands/XGROUP_CREATE.ts index 8cfd4e262e4..a04fcbeb044 100644 --- a/packages/client/lib/commands/XGROUP_CREATE.ts +++ b/packages/client/lib/commands/XGROUP_CREATE.ts @@ -1,24 +1,34 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -interface XGroupCreateOptions { - MKSTREAM?: true; +export interface XGroupCreateOptions { + MKSTREAM?: boolean; + /** + * added in 7.0 + */ + ENTRIESREAD?: number; } -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - id: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + group: RedisArgument, + id: RedisArgument, options?: XGroupCreateOptions -): RedisCommandArguments { + ) { const args = ['XGROUP', 'CREATE', key, group, id]; if (options?.MKSTREAM) { - args.push('MKSTREAM'); + args.push('MKSTREAM'); + } + + if (options?.ENTRIESREAD) { + args.push('ENTRIESREAD', options.ENTRIESREAD.toString()); } return args; -} + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; -export declare function transformReply(): RedisCommandArgument; diff --git a/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts b/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts index 62443345188..3c3ecbda0a7 100644 --- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts +++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts @@ -1,25 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XGROUP_CREATECONSUMER'; +import XGROUP_CREATECONSUMER from './XGROUP_CREATECONSUMER'; describe('XGROUP CREATECONSUMER', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group', 'consumer'), - ['XGROUP', 'CREATECONSUMER', 'key', 'group', 'consumer'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XGROUP_CREATECONSUMER.transformArguments('key', 'group', 'consumer'), + ['XGROUP', 'CREATECONSUMER', 'key', 'group', 'consumer'] + ); + }); - testUtils.testWithClient('client.xGroupCreateConsumer', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); + testUtils.testAll('xGroupCreateConsumer', async client => { + const [, reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xGroupCreateConsumer('key', 'group', 'consumer') + ]); - assert.equal( - await client.xGroupCreateConsumer('key', 'group', 'consumer'), - true - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 1); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts index 2b816a6b480..8fd21ca60de 100644 --- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, Command, NumberReply } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - consumer: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + group: RedisArgument, + consumer: RedisArgument + ) { return ['XGROUP', 'CREATECONSUMER', key, group, consumer]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts b/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts index d071aedf64f..afc524eef86 100644 --- a/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts +++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts @@ -1,23 +1,26 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XGROUP_DELCONSUMER'; +import XGROUP_DELCONSUMER from './XGROUP_DELCONSUMER'; describe('XGROUP DELCONSUMER', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group', 'consumer'), - ['XGROUP', 'DELCONSUMER', 'key', 'group', 'consumer'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XGROUP_DELCONSUMER.transformArguments('key', 'group', 'consumer'), + ['XGROUP', 'DELCONSUMER', 'key', 'group', 'consumer'] + ); + }); - testUtils.testWithClient('client.xGroupDelConsumer', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); + testUtils.testAll('xGroupDelConsumer', async client => { + const [, reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xGroupDelConsumer('key', 'group', 'consumer') + ]); - assert.equal( - await client.xGroupDelConsumer('key', 'group', 'consumer'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 0); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts index 4e4fc096d07..53007270e05 100644 --- a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - consumer: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + group: RedisArgument, + consumer: RedisArgument + ) { return ['XGROUP', 'DELCONSUMER', key, group, consumer]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XGROUP_DESTROY.spec.ts b/packages/client/lib/commands/XGROUP_DESTROY.spec.ts index ea8e7b7be98..6ec90834518 100644 --- a/packages/client/lib/commands/XGROUP_DESTROY.spec.ts +++ b/packages/client/lib/commands/XGROUP_DESTROY.spec.ts @@ -1,23 +1,26 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XGROUP_DESTROY'; +import XGROUP_DESTROY from './XGROUP_DESTROY'; describe('XGROUP DESTROY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group'), - ['XGROUP', 'DESTROY', 'key', 'group'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XGROUP_DESTROY.transformArguments('key', 'group'), + ['XGROUP', 'DESTROY', 'key', 'group'] + ); + }); - testUtils.testWithClient('client.xGroupDestroy', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); + testUtils.testAll('xGroupDestroy', async client => { + const [, reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xGroupDestroy('key', 'group') + ]); - assert.equal( - await client.xGroupDestroy('key', 'group'), - true - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 1); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XGROUP_DESTROY.ts b/packages/client/lib/commands/XGROUP_DESTROY.ts index 85910c02471..6c14d9ae2bd 100644 --- a/packages/client/lib/commands/XGROUP_DESTROY.ts +++ b/packages/client/lib/commands/XGROUP_DESTROY.ts @@ -1,12 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + group: RedisArgument + ) { return ['XGROUP', 'DESTROY', key, group]; -} - -export { transformBooleanReply as transformReply } from './generic-transformers'; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XGROUP_SETID.spec.ts b/packages/client/lib/commands/XGROUP_SETID.spec.ts index 8df51f5401d..891a796d14d 100644 --- a/packages/client/lib/commands/XGROUP_SETID.spec.ts +++ b/packages/client/lib/commands/XGROUP_SETID.spec.ts @@ -1,23 +1,26 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XGROUP_SETID'; +import XGROUP_SETID from './XGROUP_SETID'; describe('XGROUP SETID', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group', '0'), - ['XGROUP', 'SETID', 'key', 'group', '0'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XGROUP_SETID.transformArguments('key', 'group', '0'), + ['XGROUP', 'SETID', 'key', 'group', '0'] + ); + }); - testUtils.testWithClient('client.xGroupSetId', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); + testUtils.testAll('xGroupSetId', async client => { + const [, reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xGroupSetId('key', 'group', '0') + ]); - assert.equal( - await client.xGroupSetId('key', 'group', '0'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XGROUP_SETID.ts b/packages/client/lib/commands/XGROUP_SETID.ts index e732fc8d7bf..a23b4144335 100644 --- a/packages/client/lib/commands/XGROUP_SETID.ts +++ b/packages/client/lib/commands/XGROUP_SETID.ts @@ -1,13 +1,26 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - id: RedisCommandArgument -): RedisCommandArguments { - return ['XGROUP', 'SETID', key, group, id]; +export interface XGroupSetIdOptions { + /** added in 7.0 */ + ENTRIESREAD?: number; } -export declare function transformReply(): RedisCommandArgument; +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + group: RedisArgument, + id: RedisArgument, + options?: XGroupSetIdOptions + ) { + const args = ['XGROUP', 'SETID', key, group, id]; + + if (options?.ENTRIESREAD) { + args.push('ENTRIESREAD', options.ENTRIESREAD.toString()); + } + + return args; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts b/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts index a2c58999773..86abdbb1493 100644 --- a/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts +++ b/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts @@ -1,43 +1,38 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './XINFO_CONSUMERS'; +import XINFO_CONSUMERS from './XINFO_CONSUMERS'; describe('XINFO CONSUMERS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group'), - ['XINFO', 'CONSUMERS', 'key', 'group'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XINFO_CONSUMERS.transformArguments('key', 'group'), + ['XINFO', 'CONSUMERS', 'key', 'group'] + ); + }); - it('transformReply', () => { - assert.deepEqual( - transformReply([ - ['name', 'Alice', 'pending', 1, 'idle', 9104628, 'inactive', 9281221], - ['name', 'Bob', 'pending', 1, 'idle', 83841983, 'inactive', 7213871] - ]), - [{ - name: 'Alice', - pending: 1, - idle: 9104628, - inactive: 9281221, - }, { - name: 'Bob', - pending: 1, - idle: 83841983, - inactive: 7213871, - }] - ); - }); + testUtils.testAll('xInfoConsumers', async client => { + const [, , reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + // using `XREADGROUP` and not `XGROUP CREATECONSUMER` because the latter was introduced in Redis 6.2 + client.xReadGroup('group', 'consumer', { + key: 'key', + id: '0-0' + }), + client.xInfoConsumers('key', 'group') + ]); - testUtils.testWithClient('client.xInfoConsumers', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); - - assert.deepEqual( - await client.xInfoConsumers('key', 'group'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + for (const consumer of reply) { + assert.equal(typeof consumer.name, 'string'); + assert.equal(typeof consumer.pending, 'number'); + assert.equal(typeof consumer.idle, 'number'); + if (testUtils.isVersionGreaterThan([7, 2])) { + assert.equal(typeof consumer.inactive, 'number'); + } + } + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XINFO_CONSUMERS.ts b/packages/client/lib/commands/XINFO_CONSUMERS.ts index 9b3893cc93c..ca0076d6335 100644 --- a/packages/client/lib/commands/XINFO_CONSUMERS.ts +++ b/packages/client/lib/commands/XINFO_CONSUMERS.ts @@ -1,28 +1,34 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; +export type XInfoConsumersReply = ArrayReply, BlobStringReply], + [BlobStringReply<'pending'>, NumberReply], + [BlobStringReply<'idle'>, NumberReply], + /** added in 7.2 */ + [BlobStringReply<'inactive'>, NumberReply] +]>>; -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + group: RedisArgument + ) { return ['XINFO', 'CONSUMERS', key, group]; -} - -type XInfoConsumersReply = Array<{ - name: RedisCommandArgument; - pending: number; - idle: number; - inactive: number; -}>; - -export function transformReply(rawReply: Array): XInfoConsumersReply { - return rawReply.map(consumer => ({ - name: consumer[1], - pending: consumer[3], - idle: consumer[5], - inactive: consumer[7] - })); -} + }, + transformReply: { + 2: (reply: UnwrapReply>) => { + return reply.map(consumer => { + const unwrapped = consumer as unknown as UnwrapReply; + return { + name: unwrapped[1], + pending: unwrapped[3], + idle: unwrapped[5], + inactive: unwrapped[7] + }; + }); + }, + 3: undefined as unknown as () => XInfoConsumersReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/XINFO_GROUPS.spec.ts b/packages/client/lib/commands/XINFO_GROUPS.spec.ts index dea8ac58d9c..1bee02a0e6d 100644 --- a/packages/client/lib/commands/XINFO_GROUPS.spec.ts +++ b/packages/client/lib/commands/XINFO_GROUPS.spec.ts @@ -1,48 +1,36 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './XINFO_GROUPS'; +import XINFO_GROUPS from './XINFO_GROUPS'; describe('XINFO GROUPS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['XINFO', 'GROUPS', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XINFO_GROUPS.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' - }] - ); - }); - - testUtils.testWithClient('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' - }] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xInfoGroups', async client => { + const [, reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xInfoGroups('key') + ]); + + assert.deepEqual( + reply, + [{ + name: 'group', + consumers: 0, + pending: 0, + 'last-delivered-id': '0-0', + 'entries-read': testUtils.isVersionGreaterThan([7, 0]) ? null : undefined, + lag: testUtils.isVersionGreaterThan([7, 0]) ? 0 : undefined + }] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XINFO_GROUPS.ts b/packages/client/lib/commands/XINFO_GROUPS.ts index dcf504c8ce7..24661ecde84 100644 --- a/packages/client/lib/commands/XINFO_GROUPS.ts +++ b/packages/client/lib/commands/XINFO_GROUPS.ts @@ -1,25 +1,36 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, NullReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 2; +export type XInfoGroupsReply = ArrayReply, BlobStringReply], + [BlobStringReply<'consumers'>, NumberReply], + [BlobStringReply<'pending'>, NumberReply], + [BlobStringReply<'last-delivered-id'>, NumberReply], + /** added in 7.0 */ + [BlobStringReply<'entries-read'>, NumberReply | NullReply], + /** added in 7.0 */ + [BlobStringReply<'lag'>, NumberReply], +]>>; -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['XINFO', 'GROUPS', key]; -} - -type XInfoGroupsReply = Array<{ - name: RedisCommandArgument; - consumers: number; - pending: number; - lastDeliveredId: RedisCommandArgument; -}>; - -export function transformReply(rawReply: Array): XInfoGroupsReply { - return rawReply.map(group => ({ - name: group[1], - consumers: group[3], - pending: group[5], - lastDeliveredId: group[7] - })); -} + }, + transformReply: { + 2: (reply: UnwrapReply>) => { + return reply.map(group => { + const unwrapped = group as unknown as UnwrapReply; + return { + name: unwrapped[1], + consumers: unwrapped[3], + pending: unwrapped[5], + 'last-delivered-id': unwrapped[7], + 'entries-read': unwrapped[9], + lag: unwrapped[11] + }; + }); + }, + 3: undefined as unknown as () => XInfoGroupsReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/XINFO_STREAM.spec.ts b/packages/client/lib/commands/XINFO_STREAM.spec.ts index ca8d44f2875..9e6939092e2 100644 --- a/packages/client/lib/commands/XINFO_STREAM.spec.ts +++ b/packages/client/lib/commands/XINFO_STREAM.spec.ts @@ -1,72 +1,39 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './XINFO_STREAM'; +import XINFO_STREAM 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 - } - }) - } - } - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XINFO_STREAM.transformArguments('key'), + ['XINFO', 'STREAM', 'key'] + ); + }); - testUtils.testWithClient('client.xInfoStream', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); + testUtils.testAll('xInfoStream', async client => { + const [, reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xInfoStream('key') + ]); - assert.deepEqual( - await client.xInfoStream('key'), - { - length: 0, - radixTreeKeys: 0, - radixTreeNodes: 1, - groups: 1, - lastGeneratedId: '0-0', - firstEntry: null, - lastEntry: null - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, { + length: 0, + 'radix-tree-keys': 0, + 'radix-tree-nodes': 1, + 'last-generated-id': '0-0', + ...testUtils.isVersionGreaterThan([7, 0]) && { + 'max-deleted-entry-id': '0-0', + 'entries-added': 0, + 'recorded-first-entry-id': '0-0', + }, + groups: 1, + 'first-entry': null, + 'last-entry': null + }); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XINFO_STREAM.ts b/packages/client/lib/commands/XINFO_STREAM.ts index e9de25be8cb..04721d0ad32 100644 --- a/packages/client/lib/commands/XINFO_STREAM.ts +++ b/packages/client/lib/commands/XINFO_STREAM.ts @@ -1,64 +1,82 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { StreamMessageReply, transformTuplesReply } from './generic-transformers'; +import { RedisArgument, TuplesToMapReply, BlobStringReply, NumberReply, NullReply, TuplesReply, ArrayReply, UnwrapReply, Command } from '../RESP/types'; +import { isNullReply, transformTuplesReply } from './generic-transformers'; -export const FIRST_KEY_INDEX = 2; +export type XInfoStreamReply = TuplesToMapReply<[ + [BlobStringReply<'length'>, NumberReply], + [BlobStringReply<'radix-tree-keys'>, NumberReply], + [BlobStringReply<'radix-tree-nodes'>, NumberReply], + [BlobStringReply<'last-generated-id'>, BlobStringReply], + /** added in 7.2 */ + [BlobStringReply<'max-deleted-entry-id'>, BlobStringReply], + /** added in 7.2 */ + [BlobStringReply<'entries-added'>, NumberReply], + /** added in 7.2 */ + [BlobStringReply<'recorded-first-entry-id'>, BlobStringReply], + [BlobStringReply<'groups'>, NumberReply], + [BlobStringReply<'first-entry'>, ReturnType], + [BlobStringReply<'last-entry'>, ReturnType] +]>; -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['XINFO', 'STREAM', key]; -} - -interface XInfoStreamReply { - length: number; - radixTreeKeys: number; - radixTreeNodes: number; - groups: number; - lastGeneratedId: RedisCommandArgument; - firstEntry: StreamMessageReply | null; - lastEntry: StreamMessageReply | null; -} - -export function transformReply(rawReply: Array): XInfoStreamReply { - const parsedReply: Partial = {}; + }, + transformReply: { + // TODO: is there a "type safe" way to do it? + 2(reply: any) { + const parsedReply: Partial = {}; - for (let i = 0; i < rawReply.length; i+= 2) { - switch (rawReply[i]) { - case 'length': - parsedReply.length = rawReply[i + 1]; - break; + for (let i = 0; i < reply.length; i += 2) { + switch (reply[i]) { + case 'first-entry': + case 'last-entry': + parsedReply[reply[i] as ('first-entry' | 'last-entry')] = transformEntry(reply[i + 1]) as any; + break; - case 'radix-tree-keys': - parsedReply.radixTreeKeys = rawReply[i + 1]; - break; - - case 'radix-tree-nodes': - parsedReply.radixTreeNodes = rawReply[i + 1]; - break; + default: + parsedReply[reply[i] as keyof typeof parsedReply] = reply[i + 1]; + break; + } + } - case 'groups': - parsedReply.groups = rawReply[i + 1]; - break; + return parsedReply as XInfoStreamReply['DEFAULT']; + }, + 3(reply: any) { + if (reply instanceof Map) { + reply.set( + 'first-entry', + transformEntry(reply.get('first-entry')) + ); + reply.set( + 'last-entry', + transformEntry(reply.get('last-entry')) + ); + } else if (reply instanceof Array) { + reply[17] = transformEntry(reply[17]); + reply[19] = transformEntry(reply[19]); + } else { + reply['first-entry'] = transformEntry(reply['first-entry']); + reply['last-entry'] = transformEntry(reply['last-entry']); + } - case 'last-generated-id': - parsedReply.lastGeneratedId = rawReply[i + 1]; - break; + return reply as XInfoStreamReply; + } + } +} as const satisfies Command; - case 'first-entry': - parsedReply.firstEntry = rawReply[i + 1] ? { - id: rawReply[i + 1][0], - message: transformTuplesReply(rawReply[i + 1][1]) - } : null; - break; +type RawEntry = TuplesReply<[ + id: BlobStringReply, + message: ArrayReply +]> | NullReply; - case 'last-entry': - parsedReply.lastEntry = rawReply[i + 1] ? { - id: rawReply[i + 1][0], - message: transformTuplesReply(rawReply[i + 1][1]) - } : null; - break; - } - } +function transformEntry(entry: RawEntry) { + if (isNullReply(entry)) return entry; - return parsedReply as XInfoStreamReply; + const [id, message] = entry as unknown as UnwrapReply; + return { + id, + message: transformTuplesReply(message) + }; } diff --git a/packages/client/lib/commands/XLEN.spec.ts b/packages/client/lib/commands/XLEN.spec.ts index 178024ba89e..164a6d6f094 100644 --- a/packages/client/lib/commands/XLEN.spec.ts +++ b/packages/client/lib/commands/XLEN.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XLEN'; +import XLEN from './XLEN'; describe('XLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['XLEN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + XLEN.transformArguments('key'), + ['XLEN', 'key'] + ); + }); - testUtils.testWithClient('client.xLen', async client => { - assert.equal( - await client.xLen('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xLen', async client => { + assert.equal( + await client.xLen('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XLEN.ts b/packages/client/lib/commands/XLEN.ts index fda4192c8a0..d2ed566a190 100644 --- a/packages/client/lib/commands/XLEN.ts +++ b/packages/client/lib/commands/XLEN.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['XLEN', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XPENDING.spec.ts b/packages/client/lib/commands/XPENDING.spec.ts index b1fef2a217f..e6600cce383 100644 --- a/packages/client/lib/commands/XPENDING.spec.ts +++ b/packages/client/lib/commands/XPENDING.spec.ts @@ -1,62 +1,60 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XPENDING'; +import XPENDING from './XPENDING'; describe('XPENDING', () => { - describe('transformArguments', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'group'), - ['XPENDING', 'key', 'group'] - ); - }); + describe('transformArguments', () => { + it('transformArguments', () => { + assert.deepEqual( + XPENDING.transformArguments('key', 'group'), + ['XPENDING', 'key', 'group'] + ); }); + }); - describe('client.xPending', () => { - testUtils.testWithClient('simple', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); + describe('client.xPending', () => { + testUtils.testWithClient('simple', async client => { + const [, reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xPending('key', 'group') + ]); - assert.deepEqual( - await client.xPending('key', 'group'), - { - pending: 0, - firstId: null, - lastId: null, - consumers: null - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, { + pending: 0, + firstId: null, + lastId: null, + consumers: null + }); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with consumers', async client => { - const [,, id] = await Promise.all([ - client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }), - client.xGroupCreateConsumer('key', 'group', 'consumer'), - client.xAdd('key', '*', { field: 'value' }), - client.xReadGroup('group', 'consumer', { - key: 'key', - id: '>' - }) - ]); + testUtils.testWithClient('with consumers', async client => { + const [, , id, , reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xGroupCreateConsumer('key', 'group', 'consumer'), + client.xAdd('key', '*', { field: 'value' }), + client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }), + client.xPending('key', 'group') + ]); - assert.deepEqual( - await client.xPending('key', 'group'), - { - pending: 1, - firstId: id, - lastId: id, - consumers: [{ - name: 'consumer', - deliveriesCounter: 1 - }] - } - ); - }, { - ...GLOBAL.SERVERS.OPEN, - minimumDockerVersion: [6, 2] - }); + assert.deepEqual(reply, { + pending: 1, + firstId: id, + lastId: id, + consumers: [{ + name: 'consumer', + deliveriesCounter: 1 + }] + }); + }, { + ...GLOBAL.SERVERS.OPEN, + minimumDockerVersion: [6, 2] }); + }); }); diff --git a/packages/client/lib/commands/XPENDING.ts b/packages/client/lib/commands/XPENDING.ts index ac56e429410..a6ca4f5a774 100644 --- a/packages/client/lib/commands/XPENDING.ts +++ b/packages/client/lib/commands/XPENDING.ts @@ -1,44 +1,34 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, ArrayReply, TuplesReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; +type XPendingRawReply = TuplesReply<[ + pending: NumberReply, + firstId: BlobStringReply | NullReply, + lastId: BlobStringReply | NullReply, + consumers: ArrayReply> | NullReply +]>; -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, group: RedisArgument) { return ['XPENDING', key, group]; -} - -type XPendingRawReply = [ - pending: number, - firstId: RedisCommandArgument | null, - lastId: RedisCommandArgument | null, - consumers: Array<[ - name: RedisCommandArgument, - deliveriesCounter: RedisCommandArgument - ]> | null -]; - -interface XPendingReply { - pending: number; - firstId: RedisCommandArgument | null; - lastId: RedisCommandArgument | null; - consumers: Array<{ - name: RedisCommandArgument; - deliveriesCounter: number; - }> | null; -} - -export function transformReply(reply: XPendingRawReply): XPendingReply { + }, + transformReply(reply: UnwrapReply) { + const consumers = reply[3] as unknown as UnwrapReply; return { - pending: reply[0], - firstId: reply[1], - lastId: reply[2], - consumers: reply[3] === null ? null : reply[3].map(([name, deliveriesCounter]) => ({ - name, - deliveriesCounter: Number(deliveriesCounter) - })) - }; -} + pending: reply[0], + firstId: reply[1], + lastId: reply[2], + consumers: consumers === null ? null : consumers.map(consumer => { + const [name, deliveriesCounter] = consumer as unknown as UnwrapReply; + return { + name, + deliveriesCounter: Number(deliveriesCounter) + }; + }) + } + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/XPENDING_RANGE.spec.ts b/packages/client/lib/commands/XPENDING_RANGE.spec.ts index 0b57c704bb0..a66484fd2e6 100644 --- a/packages/client/lib/commands/XPENDING_RANGE.spec.ts +++ b/packages/client/lib/commands/XPENDING_RANGE.spec.ts @@ -1,53 +1,66 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XPENDING_RANGE'; +import XPENDING_RANGE from './XPENDING_RANGE'; describe('XPENDING RANGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 'group', '-', '+', 1), - ['XPENDING', 'key', 'group', '-', '+', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + XPENDING_RANGE.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 IDLE', () => { + assert.deepEqual( + XPENDING_RANGE.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 consumer', () => { + assert.deepEqual( + XPENDING_RANGE.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'] - ); - }); + it('with IDLE, consumer', () => { + assert.deepEqual( + XPENDING_RANGE.transformArguments('key', 'group', '-', '+', 1, { + IDLE: 1, + consumer: 'consumer' + }), + ['XPENDING', 'key', 'group', 'IDLE', '1', '-', '+', '1', 'consumer'] + ); }); + }); - testUtils.testWithClient('client.xPendingRange', async client => { - await client.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }); + testUtils.testAll('xPendingRange', async client => { + const [, id, , reply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xAdd('key', '*', { field: 'value' }), + client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }), + client.xPendingRange('key', 'group', '-', '+', 1) + ]); - assert.deepEqual( - await client.xPendingRange('key', 'group', '-', '+', 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + assert.ok(Array.isArray(reply)); + assert.equal(reply.length, 1); + assert.equal(reply[0].id, id); + assert.equal(reply[0].consumer, 'consumer'); + assert.equal(typeof reply[0].millisecondsSinceLastDelivery, 'number'); + assert.equal(reply[0].deliveriesCounter, 1); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XPENDING_RANGE.ts b/packages/client/lib/commands/XPENDING_RANGE.ts index 87660de545d..60a28e5172d 100644 --- a/packages/client/lib/commands/XPENDING_RANGE.ts +++ b/packages/client/lib/commands/XPENDING_RANGE.ts @@ -1,56 +1,55 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -interface XPendingRangeOptions { - IDLE?: number; - consumer?: RedisCommandArgument; +export interface XPendingRangeOptions { + IDLE?: number; + consumer?: RedisArgument; } -export function transformArguments( - key: RedisCommandArgument, - group: RedisCommandArgument, - start: string, - end: string, +type XPendingRangeRawReply = ArrayReply>; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + group: RedisArgument, + start: RedisArgument, + end: RedisArgument, count: number, options?: XPendingRangeOptions -): RedisCommandArguments { + ) { const args = ['XPENDING', key, group]; - if (options?.IDLE) { - args.push('IDLE', options.IDLE.toString()); + if (options?.IDLE !== undefined) { + args.push('IDLE', options.IDLE.toString()); } - args.push(start, end, count.toString()); + args.push( + start, + end, + count.toString() + ); if (options?.consumer) { - args.push(options.consumer); + args.push(options.consumer); } return args; -} - -type XPendingRangeRawReply = Array<[ - id: RedisCommandArgument, - consumer: RedisCommandArgument, - millisecondsSinceLastDelivery: number, - deliveriesCounter: number -]>; - -type XPendingRangeReply = Array<{ - id: RedisCommandArgument; - owner: RedisCommandArgument; - millisecondsSinceLastDelivery: number; - deliveriesCounter: number; -}>; - -export function transformReply(reply: XPendingRangeRawReply): XPendingRangeReply { - return reply.map(([id, owner, millisecondsSinceLastDelivery, deliveriesCounter]) => ({ - id, - owner, - millisecondsSinceLastDelivery, - deliveriesCounter - })); -} + }, + transformReply(reply: UnwrapReply) { + return reply.map(pending => { + const unwrapped = pending as unknown as UnwrapReply; + return { + id: unwrapped[0], + consumer: unwrapped[1], + millisecondsSinceLastDelivery: unwrapped[2], + deliveriesCounter: unwrapped[3] + }; + }); + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/XRANGE.spec.ts b/packages/client/lib/commands/XRANGE.spec.ts index 01c713e9595..ebfe271db86 100644 --- a/packages/client/lib/commands/XRANGE.spec.ts +++ b/packages/client/lib/commands/XRANGE.spec.ts @@ -1,30 +1,45 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XRANGE'; +import XRANGE from './XRANGE'; describe('XRANGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', '-', '+'), - ['XRANGE', 'key', '-', '+'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + XRANGE.transformArguments('key', '-', '+'), + ['XRANGE', 'key', '-', '+'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + XRANGE.transformArguments('key', '-', '+', { + COUNT: 1 + }), + ['XRANGE', 'key', '-', '+', 'COUNT', '1'] + ); + }); + }); - it('with COUNT', () => { - assert.deepEqual( - transformArguments('key', '-', '+', { - COUNT: 1 - }), - ['XRANGE', 'key', '-', '+', 'COUNT', '1'] - ); - }); + testUtils.testAll('xRange', async client => { + const message = Object.create(null, { + field: { + value: 'value', + enumerable: true + } }); - testUtils.testWithClient('client.xRange', async client => { - assert.deepEqual( - await client.xRange('key', '+', '-'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + const [id, reply] = await Promise.all([ + client.xAdd('key', '*', message), + client.xRange('key', '-', '+') + ]); + + assert.deepEqual(reply, [{ + id, + message + }]); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XRANGE.ts b/packages/client/lib/commands/XRANGE.ts index ae56639f769..fb65160d810 100644 --- a/packages/client/lib/commands/XRANGE.ts +++ b/packages/client/lib/commands/XRANGE.ts @@ -1,26 +1,35 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, ArrayReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { StreamMessageRawReply, transformStreamMessageReply } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -interface XRangeOptions { - COUNT?: number; +export interface XRangeOptions { + COUNT?: number; } -export function transformArguments( - key: RedisCommandArgument, - start: RedisCommandArgument, - end: RedisCommandArgument, - options?: XRangeOptions -): RedisCommandArguments { - const args = ['XRANGE', key, start, end]; +export function transformXRangeArguments( + command: RedisArgument, + key: RedisArgument, + start: RedisArgument, + end: RedisArgument, + options?: XRangeOptions +) { + const args = [command, key, start, end]; - if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); - } + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } - return args; + return args; } -export { transformStreamMessagesReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments: transformXRangeArguments.bind(undefined, 'XRANGE'), + transformReply( + reply: UnwrapReply>, + preserve?: any, + typeMapping?: TypeMapping + ) { + return reply.map(transformStreamMessageReply.bind(undefined, typeMapping)); + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/XREAD.spec.ts b/packages/client/lib/commands/XREAD.spec.ts index b607f53532e..09784a56e6d 100644 --- a/packages/client/lib/commands/XREAD.spec.ts +++ b/packages/client/lib/commands/XREAD.spec.ts @@ -1,103 +1,134 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { FIRST_KEY_INDEX, transformArguments } from './XREAD'; +import XREAD from './XREAD'; describe('XREAD', () => { - describe('FIRST_KEY_INDEX', () => { - it('single stream', () => { - assert.equal( - FIRST_KEY_INDEX({ key: 'key', id: '' }), - 'key' - ); - }); + describe('FIRST_KEY_INDEX', () => { + it('single stream', () => { + assert.equal( + XREAD.FIRST_KEY_INDEX({ + key: 'key', + id: '' + }), + 'key' + ); + }); - it('multiple streams', () => { - assert.equal( - FIRST_KEY_INDEX([{ key: '1', id: '' }, { key: '2', id: '' }]), - '1' - ); - }); + it('multiple streams', () => { + assert.equal( + XREAD.FIRST_KEY_INDEX([{ + key: '1', + id: '' + }, { + key: '2', + id: '' + }]), + '1' + ); }); + }); - describe('transformArguments', () => { - it('single stream', () => { - assert.deepEqual( - transformArguments({ - key: 'key', - id: '0' - }), - ['XREAD', 'STREAMS', 'key', '0'] - ); - }); + describe('transformArguments', () => { + it('single stream', () => { + assert.deepEqual( + XREAD.transformArguments({ + key: 'key', + id: '0-0' + }), + ['XREAD', 'STREAMS', 'key', '0-0'] + ); + }); - it('multiple streams', () => { - assert.deepEqual( - transformArguments([{ - key: '1', - id: '0' - }, { - key: '2', - id: '0' - }]), - ['XREAD', 'STREAMS', '1', '2', '0', '0'] - ); - }); + it('multiple streams', () => { + assert.deepEqual( + XREAD.transformArguments([{ + key: '1', + id: '0-0' + }, { + key: '2', + id: '0-0' + }]), + ['XREAD', 'STREAMS', '1', '2', '0-0', '0-0'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + XREAD.transformArguments({ + key: 'key', + id: '0-0' + }, { + COUNT: 1 + }), + ['XREAD', 'COUNT', '1', 'STREAMS', 'key', '0-0'] + ); + }); + + it('with BLOCK', () => { + assert.deepEqual( + XREAD.transformArguments({ + key: 'key', + id: '0-0' + }, { + BLOCK: 0 + }), + ['XREAD', 'BLOCK', '0', 'STREAMS', 'key', '0-0'] + ); + }); + + it('with COUNT, BLOCK', () => { + assert.deepEqual( + XREAD.transformArguments({ + key: 'key', + id: '0-0' + }, { + COUNT: 1, + BLOCK: 0 + }), + ['XREAD', 'COUNT', '1', 'BLOCK', '0', 'STREAMS', 'key', '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'] - ); - }); + testUtils.testAll('client.xRead', async client => { + const message = { field: 'value' }, + [id, reply] = await Promise.all([ + client.xAdd('key', '*', message), + client.xRead({ + key: 'key', + id: '0-0' + }), + ]) - it('with COUNT, BLOCK', () => { - assert.deepEqual( - transformArguments({ - key: 'key', - id: '0' - }, { - COUNT: 1, - BLOCK: 0 - }), - ['XREAD', 'COUNT', '1', 'BLOCK', '0', 'STREAMS', 'key', '0'] - ); - }); + // FUTURE resp3 compatible + const obj = Object.assign(Object.create(null), { + 'key': [{ + id: id, + message: Object.create(null, { + field: { + value: 'value', + configurable: true, + enumerable: true + } + }) + }] }); - testUtils.testWithClient('client.xRead', async client => { - assert.equal( - await client.xRead({ - key: 'key', - id: '0' - }), - null - ); - }, GLOBAL.SERVERS.OPEN); + // v4 compatible + const expected = [{ + name: 'key', + messages: [{ + id: id, + message: Object.assign(Object.create(null), { + field: 'value' + }) + }] + }]; - testUtils.testWithCluster('cluster.xRead', async cluster => { - assert.equal( - await cluster.xRead({ - key: 'key', - id: '0' - }), - null - ); - }, GLOBAL.CLUSTERS.OPEN); + assert.deepStrictEqual(reply, expected); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XREAD.ts b/packages/client/lib/commands/XREAD.ts index e5f85dbe7fe..97679376c19 100644 --- a/packages/client/lib/commands/XREAD.ts +++ b/packages/client/lib/commands/XREAD.ts @@ -1,46 +1,58 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { Command, RedisArgument, ReplyUnion } from '../RESP/types'; +import { transformStreamsMessagesReplyResp2 } from './generic-transformers'; -export const FIRST_KEY_INDEX = (streams: Array | XReadStream): RedisCommandArgument => { - return Array.isArray(streams) ? streams[0].key : streams.key; -}; +export interface XReadStream { + key: RedisArgument; + id: RedisArgument; +} -export const IS_READ_ONLY = true; +export type XReadStreams = Array | XReadStream; -interface XReadStream { - key: RedisCommandArgument; - id: RedisCommandArgument; +export function pushXReadStreams(args: Array, streams: XReadStreams) { + args.push('STREAMS'); + + if (Array.isArray(streams)) { + const keysStart = args.length, + idsStart = keysStart + streams.length; + for (let i = 0; i < streams.length; i++) { + const stream = streams[i]; + args[keysStart + i] = stream.key; + args[idsStart + i] = stream.id; + } + } else { + args.push(streams.key, streams.id); + } } -interface XReadOptions { - COUNT?: number; - BLOCK?: number; +export interface XReadOptions { + COUNT?: number; + BLOCK?: number; } -export function transformArguments( - streams: Array | XReadStream, - options?: XReadOptions -): RedisCommandArguments { - const args: RedisCommandArguments = ['XREAD']; +export default { + FIRST_KEY_INDEX(streams: XReadStreams) { + return Array.isArray(streams) ? streams[0].key : streams.key; + }, + IS_READ_ONLY: true, + transformArguments(streams: XReadStreams, options?: XReadOptions) { + const args: Array = ['XREAD']; if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + args.push('COUNT', options.COUNT.toString()); } - if (typeof options?.BLOCK === 'number') { - args.push('BLOCK', options.BLOCK.toString()); + if (options?.BLOCK !== undefined) { + 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; - } + pushXReadStreams(args, streams); return args; -} + }, + transformReply: { + 2: transformStreamsMessagesReplyResp2, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true +} as const satisfies Command; -export { transformStreamsMessagesReply as transformReply } from './generic-transformers'; diff --git a/packages/client/lib/commands/XREADGROUP.spec.ts b/packages/client/lib/commands/XREADGROUP.spec.ts index fa196d504ad..004a48ddbe3 100644 --- a/packages/client/lib/commands/XREADGROUP.spec.ts +++ b/packages/client/lib/commands/XREADGROUP.spec.ts @@ -1,153 +1,157 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { FIRST_KEY_INDEX, transformArguments } from './XREADGROUP'; +import XREADGROUP from './XREADGROUP'; describe('XREADGROUP', () => { - describe('FIRST_KEY_INDEX', () => { - it('single stream', () => { - assert.equal( - FIRST_KEY_INDEX('', '', { key: 'key', id: '' }), - 'key' - ); - }); + describe('FIRST_KEY_INDEX', () => { + it('single stream', () => { + assert.equal( + XREADGROUP.FIRST_KEY_INDEX('', '', { key: 'key', id: '' }), + 'key' + ); + }); - it('multiple streams', () => { - assert.equal( - FIRST_KEY_INDEX('', '', [{ key: '1', id: '' }, { key: '2', id: '' }]), - '1' - ); - }); + it('multiple streams', () => { + assert.equal( + XREADGROUP.FIRST_KEY_INDEX('', '', [{ key: '1', id: '' }, { key: '2', id: '' }]), + '1' + ); }); + }); - describe('transformArguments', () => { - it('single stream', () => { - assert.deepEqual( - transformArguments('group', 'consumer', { - key: 'key', - id: '0' - }), - ['XREADGROUP', 'GROUP', 'group', 'consumer', 'STREAMS', 'key', '0'] - ); - }); + describe('transformArguments', () => { + it('single stream', () => { + assert.deepEqual( + XREADGROUP.transformArguments('group', 'consumer', { + key: 'key', + id: '0-0' + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'STREAMS', 'key', '0-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('multiple streams', () => { + assert.deepEqual( + XREADGROUP.transformArguments('group', 'consumer', [{ + key: '1', + id: '0-0' + }, { + key: '2', + id: '0-0' + }]), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'STREAMS', '1', '2', '0-0', '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 COUNT', () => { + assert.deepEqual( + XREADGROUP.transformArguments('group', 'consumer', { + key: 'key', + id: '0-0' + }, { + COUNT: 1 + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'COUNT', '1', 'STREAMS', 'key', '0-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 BLOCK', () => { + assert.deepEqual( + XREADGROUP.transformArguments('group', 'consumer', { + key: 'key', + id: '0-0' + }, { + BLOCK: 0 + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'BLOCK', '0', 'STREAMS', 'key', '0-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 NOACK', () => { + assert.deepEqual( + XREADGROUP.transformArguments('group', 'consumer', { + key: 'key', + id: '0-0' + }, { + NOACK: true + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'NOACK', 'STREAMS', 'key', '0-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'] - ); - }); + it('with COUNT, BLOCK, NOACK', () => { + assert.deepEqual( + XREADGROUP.transformArguments('group', 'consumer', { + key: 'key', + id: '0-0' + }, { + COUNT: 1, + BLOCK: 0, + NOACK: true + }), + ['XREADGROUP', 'GROUP', 'group', 'consumer', 'COUNT', '1', 'BLOCK', '0', 'NOACK', 'STREAMS', 'key', '0-0'] + ); }); + }); + + testUtils.testAll('xReadGroup - null', async client => { + const [, readGroupReply] = await Promise.all([ + client.xGroupCreate('key', 'group', '$', { + MKSTREAM: true + }), + client.xReadGroup('group', 'consumer', { + key: 'key', + id: '>' + }) + ]); - describe('client.xReadGroup', () => { - testUtils.testWithClient('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); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); - assert.equal(readGroupReply, null); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xReadGroup - 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: '>' + }) + ]); - testUtils.testWithClient('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 - } - }) - }] - }]); - }, GLOBAL.SERVERS.OPEN); + // FUTURE resp3 compatible + const obj = Object.assign(Object.create(null), { + 'key': [{ + id: id, + message: Object.create(null, { + field: { + value: 'value', + configurable: true, + enumerable: true + } + }) + }] }); - testUtils.testWithCluster('cluster.xReadGroup', async cluster => { - const [, readGroupReply] = await Promise.all([ - cluster.xGroupCreate('key', 'group', '$', { - MKSTREAM: true - }), - cluster.xReadGroup('group', 'consumer', { - key: 'key', - id: '>' - }) - ]); + // v4 compatible + const expected = [{ + name: 'key', + messages: [{ + id: id, + message: Object.assign(Object.create(null), { + field: 'value' + }) + }] + }]; - assert.equal(readGroupReply, null); - }, GLOBAL.CLUSTERS.OPEN); + assert.deepStrictEqual(readGroupReply, expected); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XREADGROUP.ts b/packages/client/lib/commands/XREADGROUP.ts index e90e698a2ad..296480f9e3a 100644 --- a/packages/client/lib/commands/XREADGROUP.ts +++ b/packages/client/lib/commands/XREADGROUP.ts @@ -1,57 +1,49 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export interface XReadGroupStream { - key: RedisCommandArgument; - id: RedisCommandArgument; -} +import { Command, RedisArgument, ReplyUnion } from '../RESP/types'; +import { transformStreamsMessagesReplyResp2 } from './generic-transformers'; +import XREAD, { XReadStreams, pushXReadStreams } from './XREAD'; export interface XReadGroupOptions { - COUNT?: number; - BLOCK?: number; - NOACK?: true; + COUNT?: number; + BLOCK?: number; + NOACK?: boolean; } -export const FIRST_KEY_INDEX = ( - _group: RedisCommandArgument, - _consumer: RedisCommandArgument, - streams: Array | XReadGroupStream -): RedisCommandArgument => { - return Array.isArray(streams) ? streams[0].key : streams.key; -}; - -export const IS_READ_ONLY = true; - -export function transformArguments( - group: RedisCommandArgument, - consumer: RedisCommandArgument, - streams: Array | XReadGroupStream, +export default { + FIRST_KEY_INDEX( + _group: RedisArgument, + _consumer: RedisArgument, + streams: XReadStreams + ) { + return XREAD.FIRST_KEY_INDEX(streams); + }, + IS_READ_ONLY: true, + transformArguments( + group: RedisArgument, + consumer: RedisArgument, + streams: XReadStreams, options?: XReadGroupOptions -): RedisCommandArguments { + ) { const args = ['XREADGROUP', 'GROUP', group, consumer]; - if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + if (options?.COUNT !== undefined) { + args.push('COUNT', options.COUNT.toString()); } - if (typeof options?.BLOCK === 'number') { - args.push('BLOCK', options.BLOCK.toString()); + if (options?.BLOCK !== undefined) { + args.push('BLOCK', options.BLOCK.toString()); } if (options?.NOACK) { - args.push('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; - } + pushXReadStreams(args, streams); return args; -} - -export { transformStreamsMessagesReply as transformReply } from './generic-transformers'; + }, + transformReply: { + 2: transformStreamsMessagesReplyResp2, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true, +} as const satisfies Command; diff --git a/packages/client/lib/commands/XREVRANGE.spec.ts b/packages/client/lib/commands/XREVRANGE.spec.ts index fd6e1a3adfe..c9f3043af38 100644 --- a/packages/client/lib/commands/XREVRANGE.spec.ts +++ b/packages/client/lib/commands/XREVRANGE.spec.ts @@ -1,30 +1,45 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XREVRANGE'; +import XREVRANGE from './XREVRANGE'; describe('XREVRANGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', '-', '+'), - ['XREVRANGE', 'key', '-', '+'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + XREVRANGE.transformArguments('key', '-', '+'), + ['XREVRANGE', 'key', '-', '+'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + XREVRANGE.transformArguments('key', '-', '+', { + COUNT: 1 + }), + ['XREVRANGE', 'key', '-', '+', 'COUNT', '1'] + ); + }); + }); - it('with COUNT', () => { - assert.deepEqual( - transformArguments('key', '-', '+', { - COUNT: 1 - }), - ['XREVRANGE', 'key', '-', '+', 'COUNT', '1'] - ); - }); + testUtils.testAll('xRevRange', async client => { + const message = Object.create(null, { + field: { + value: 'value', + enumerable: true + } }); - testUtils.testWithClient('client.xRevRange', async client => { - assert.deepEqual( - await client.xRevRange('key', '+', '-'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + const [id, reply] = await Promise.all([ + client.xAdd('key', '*', message), + client.xRange('key', '-', '+') + ]); + + assert.deepEqual(reply, [{ + id, + message + }]); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/XREVRANGE.ts b/packages/client/lib/commands/XREVRANGE.ts index 96bbeba83ce..86e4d8abc9e 100644 --- a/packages/client/lib/commands/XREVRANGE.ts +++ b/packages/client/lib/commands/XREVRANGE.ts @@ -1,26 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { Command } from '../RESP/types'; +import XRANGE, { transformXRangeArguments } from './XRANGE'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -interface XRangeRevOptions { - COUNT?: number; -} - -export function transformArguments( - key: RedisCommandArgument, - start: RedisCommandArgument, - end: RedisCommandArgument, - options?: XRangeRevOptions -): RedisCommandArguments { - const args = ['XREVRANGE', key, start, end]; - - if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); - } - - return args; +export interface XRevRangeOptions { + COUNT?: number; } -export { transformStreamMessagesReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: XRANGE.FIRST_KEY_INDEX, + IS_READ_ONLY: XRANGE.IS_READ_ONLY, + transformArguments: transformXRangeArguments.bind(undefined, 'XREVRANGE'), + transformReply: XRANGE.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/XSETID.spec.ts b/packages/client/lib/commands/XSETID.spec.ts index e69de29bb2d..5ebbc943816 100644 --- a/packages/client/lib/commands/XSETID.spec.ts +++ b/packages/client/lib/commands/XSETID.spec.ts @@ -0,0 +1,46 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import XSETID from './XSETID'; + +describe('XSETID', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + XSETID.transformArguments('key', '0-0'), + ['XSETID', 'key', '0-0'] + ); + }); + + it('with ENTRIESADDED', () => { + assert.deepEqual( + XSETID.transformArguments('key', '0-0', { + ENTRIESADDED: 1 + }), + ['XSETID', 'key', '0-0', 'ENTRIESADDED', '1'] + ); + }); + + it('with MAXDELETEDID', () => { + assert.deepEqual( + XSETID.transformArguments('key', '0-0', { + MAXDELETEDID: '1-1' + }), + ['XSETID', 'key', '0-0', 'MAXDELETEDID', '1-1'] + ); + }); + }); + + testUtils.testAll('xSetId', async client => { + const id = await client.xAdd('key', '*', { + field: 'value' + }); + + assert.equal( + await client.xSetId('key', id), + 'OK' + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/XSETID.ts b/packages/client/lib/commands/XSETID.ts index 76acc7ebab4..a2ac8af0011 100644 --- a/packages/client/lib/commands/XSETID.ts +++ b/packages/client/lib/commands/XSETID.ts @@ -1,28 +1,31 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; - -export const FIRST_KEY_INDEX = 1; - -interface XSetIdOptions { - ENTRIESADDED?: number; - MAXDELETEDID?: RedisCommandArgument; +import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +export interface XSetIdOptions { + /** added in 7.0 */ + ENTRIESADDED?: number; + /** added in 7.0 */ + MAXDELETEDID?: RedisArgument; } -export function transformArguments( - key: RedisCommandArgument, - lastId: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + lastId: RedisArgument, options?: XSetIdOptions -): RedisCommandArguments { + ) { const args = ['XSETID', key, lastId]; if (options?.ENTRIESADDED) { - args.push('ENTRIESADDED', options.ENTRIESADDED.toString()); + args.push('ENTRIESADDED', options.ENTRIESADDED.toString()); } if (options?.MAXDELETEDID) { - args.push('MAXDELETEDID', options.MAXDELETEDID); + args.push('MAXDELETEDID', options.MAXDELETEDID); } return args; -} + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; -export declare function transformReply(): 'OK'; diff --git a/packages/client/lib/commands/XTRIM.spec.ts b/packages/client/lib/commands/XTRIM.spec.ts index a8f8078eb28..a5a2fdf23c5 100644 --- a/packages/client/lib/commands/XTRIM.spec.ts +++ b/packages/client/lib/commands/XTRIM.spec.ts @@ -1,49 +1,52 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './XTRIM'; +import XTRIM from './XTRIM'; describe('XTRIM', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 'MAXLEN', 1), - ['XTRIM', 'key', 'MAXLEN', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + XTRIM.transformArguments('key', 'MAXLEN', 1), + ['XTRIM', 'key', 'MAXLEN', '1'] + ); + }); - it('with strategyModifier', () => { - assert.deepEqual( - transformArguments('key', 'MAXLEN', 1, { - strategyModifier: '=' - }), - ['XTRIM', 'key', 'MAXLEN', '=', '1'] - ); - }); + it('with strategyModifier', () => { + assert.deepEqual( + XTRIM.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 LIMIT', () => { + assert.deepEqual( + XTRIM.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'] - ); - }); + it('with strategyModifier, LIMIT', () => { + assert.deepEqual( + XTRIM.transformArguments('key', 'MAXLEN', 1, { + strategyModifier: '=', + LIMIT: 1 + }), + ['XTRIM', 'key', 'MAXLEN', '=', '1', 'LIMIT', '1'] + ); }); + }); - testUtils.testWithClient('client.xTrim', async client => { - assert.equal( - await client.xTrim('key', 'MAXLEN', 1), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('xTrim', async client => { + assert.equal( + await client.xTrim('key', 'MAXLEN', 1), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN, + }); }); diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts index 15b934c56ef..0512323a32a 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -1,31 +1,33 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -interface XTrimOptions { - strategyModifier?: '=' | '~'; - LIMIT?: number; +export interface XTrimOptions { + strategyModifier?: '=' | '~'; + /** added in 6.2 */ + LIMIT?: number; } -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, strategy: 'MAXLEN' | 'MINID', threshold: number, options?: XTrimOptions -): RedisCommandArguments { + ) { const args = ['XTRIM', key, strategy]; if (options?.strategyModifier) { - args.push(options.strategyModifier); + args.push(options.strategyModifier); } args.push(threshold.toString()); if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.toString()); + args.push('LIMIT', options.LIMIT.toString()); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZADD.spec.ts b/packages/client/lib/commands/ZADD.spec.ts index 4f497bdca90..5f64cb13b92 100644 --- a/packages/client/lib/commands/ZADD.spec.ts +++ b/packages/client/lib/commands/ZADD.spec.ts @@ -1,127 +1,145 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZADD'; +import ZADD from './ZADD'; describe('ZADD', () => { - describe('transformArguments', () => { - it('single member', () => { - assert.deepEqual( - transformArguments('key', { - value: '1', - score: 1 - }), - ['ZADD', 'key', '1', '1'] - ); - }); + describe('transformArguments', () => { + it('single member', () => { + assert.deepEqual( + ZADD.transformArguments('key', { + value: '1', + score: 1 + }), + ['ZADD', 'key', '1', '1'] + ); + }); + + it('multiple members', () => { + assert.deepEqual( + ZADD.transformArguments('key', [{ + value: '1', + score: 1 + }, { + value: '2', + score: 2 + }]), + ['ZADD', 'key', '1', '1', '2', '2'] + ); + }); - it('multiple members', () => { - assert.deepEqual( - transformArguments('key', [{ - value: '1', - score: 1 - }, { - value: '2', - score: 2 - }]), - ['ZADD', 'key', '1', '1', '2', '2'] - ); - }); + describe('with condition', () => { + it('condition property', () => { + assert.deepEqual( + ZADD.transformArguments('key', { + value: '1', + score: 1 + }, { + condition: 'NX' + }), + ['ZADD', 'key', 'NX', '1', '1'] + ); + }); - it('with NX', () => { - assert.deepEqual( - transformArguments('key', { - value: '1', - score: 1 - }, { - NX: true - }), - ['ZADD', 'key', 'NX', '1', '1'] - ); - }); + it('with NX (backwards compatibility)', () => { + assert.deepEqual( + ZADD.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 XX (backwards compatibility)', () => { + assert.deepEqual( + ZADD.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'] - ); - }); + describe('with comparison', () => { + it('with LT', () => { + assert.deepEqual( + ZADD.transformArguments('key', { + value: '1', + score: 1 + }, { + comparison: 'LT' + }), + ['ZADD', 'key', 'LT', '1', '1'] + ); + }); - it('with LT', () => { - assert.deepEqual( - transformArguments('key', { - value: '1', - score: 1 - }, { - LT: true - }), - ['ZADD', 'key', 'LT', '1', '1'] - ); - }); + it('with LT (backwards compatibility)', () => { + assert.deepEqual( + ZADD.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 GT (backwards compatibility)', () => { + assert.deepEqual( + ZADD.transformArguments('key', { + value: '1', + score: 1 + }, { + GT: true + }), + ['ZADD', 'key', 'GT', '1', '1'] + ); + }); + }); - it('with INCR', () => { - assert.deepEqual( - transformArguments('key', { - value: '1', - score: 1 - }, { - INCR: true - }), - ['ZADD', 'key', 'INCR', '1', '1'] - ); - }); + it('with CH', () => { + assert.deepEqual( + ZADD.transformArguments('key', { + value: '1', + score: 1 + }, { + CH: true + }), + ['ZADD', 'key', 'CH', '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'] - ); - }); + it('with condition, comparison, CH', () => { + assert.deepEqual( + ZADD.transformArguments('key', { + value: '1', + score: 1 + }, { + condition: 'XX', + comparison: 'LT', + CH: true + }), + ['ZADD', 'key', 'XX', 'LT', 'CH', '1', '1'] + ); }); + }); - testUtils.testWithClient('client.zAdd', async client => { - assert.equal( - await client.zAdd('key', { - value: '1', - score: 1 - }), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zAdd', async client => { + assert.equal( + await client.zAdd('key', { + value: 'a', + score: 1 + }), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZADD.ts b/packages/client/lib/commands/ZADD.ts index 9ac67d59cce..0c5602bf988 100644 --- a/packages/client/lib/commands/ZADD.ts +++ b/packages/client/lib/commands/ZADD.ts @@ -1,71 +1,82 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformNumberInfinityArgument, 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; +import { RedisArgument, Command } from '../RESP/types'; +import { SortedSetMember, transformDoubleArgument, transformDoubleReply } from './generic-transformers'; + +export interface ZAddOptions { + condition?: 'NX' | 'XX'; + /** + * @deprecated Use `{ condition: 'NX' }` instead. + */ + NX?: boolean; + /** + * @deprecated Use `{ condition: 'XX' }` instead. + */ + XX?: boolean; + comparison?: 'LT' | 'GT'; + /** + * @deprecated Use `{ comparison: 'LT' }` instead. + */ + LT?: boolean; + /** + * @deprecated Use `{ comparison: 'GT' }` instead. + */ + GT?: boolean; + CH?: boolean; } -interface INCR { - INCR?: true; -} - -type ZAddOptions = (NX | (XX & LT & GT)) & CH & INCR; - -export function transformArguments( - key: RedisCommandArgument, - members: ZMember | Array, +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + members: SortedSetMember | Array, options?: ZAddOptions -): RedisCommandArguments { + ) { 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?.condition) { + args.push(options.condition); + } else if (options?.NX) { + args.push('NX'); + } else if (options?.XX) { + args.push('XX'); + } + + if (options?.comparison) { + args.push(options.comparison); + } else if (options?.LT) { + args.push('LT'); + } else if (options?.GT) { + args.push('GT'); } - if ((options)?.CH) { - args.push('CH'); + if (options?.CH) { + args.push('CH'); } - if ((options)?.INCR) { - args.push('INCR'); - } - - for (const { score, value } of (Array.isArray(members) ? members : [members])) { - args.push( - transformNumberInfinityArgument(score), - value - ); - } + pushMembers(args, members); return args; + }, + transformReply: transformDoubleReply +} as const satisfies Command; + +export function pushMembers( + args: Array, + members: SortedSetMember | Array) { + if (Array.isArray(members)) { + for (const member of members) { + pushMember(args, member); + } + } else { + pushMember(args, members); + } } -export { transformNumberInfinityReply as transformReply } from './generic-transformers'; +function pushMember( + args: Array, + member: SortedSetMember +) { + args.push( + transformDoubleArgument(member.score), + member.value + ); +} diff --git a/packages/client/lib/commands/ZADD_INCR.spec.ts b/packages/client/lib/commands/ZADD_INCR.spec.ts new file mode 100644 index 00000000000..c6ffcb796f3 --- /dev/null +++ b/packages/client/lib/commands/ZADD_INCR.spec.ts @@ -0,0 +1,93 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ZADD_INCR from './ZADD_INCR'; + +describe('ZADD INCR', () => { + describe('transformArguments', () => { + it('single member', () => { + assert.deepEqual( + ZADD_INCR.transformArguments('key', { + value: '1', + score: 1 + }), + ['ZADD', 'key', 'INCR', '1', '1'] + ); + }); + + it('multiple members', () => { + assert.deepEqual( + ZADD_INCR.transformArguments('key', [{ + value: '1', + score: 1 + }, { + value: '2', + score: 2 + }]), + ['ZADD', 'key', 'INCR', '1', '1', '2', '2'] + ); + }); + + it('with condition', () => { + assert.deepEqual( + ZADD_INCR.transformArguments('key', { + value: '1', + score: 1 + }, { + condition: 'NX' + }), + ['ZADD', 'key', 'NX', 'INCR', '1', '1'] + ); + }); + + it('with comparison', () => { + assert.deepEqual( + ZADD_INCR.transformArguments('key', { + value: '1', + score: 1 + }, { + comparison: 'LT' + }), + ['ZADD', 'key', 'LT', 'INCR', '1', '1'] + ); + }); + + it('with CH', () => { + assert.deepEqual( + ZADD_INCR.transformArguments('key', { + value: '1', + score: 1 + }, { + CH: true + }), + ['ZADD', 'key', 'CH', 'INCR', '1', '1'] + ); + }); + + it('with condition, comparison, CH', () => { + assert.deepEqual( + ZADD_INCR.transformArguments('key', { + value: '1', + score: 1 + }, { + condition: 'XX', + comparison: 'LT', + CH: true + }), + ['ZADD', 'key', 'XX', 'LT', 'CH', 'INCR', '1', '1'] + ); + }); + }); + + testUtils.testAll('zAddIncr', async client => { + assert.equal( + await client.zAddIncr('key', { + value: 'a', + score: 1 + }), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/ZADD_INCR.ts b/packages/client/lib/commands/ZADD_INCR.ts new file mode 100644 index 00000000000..8fb10721674 --- /dev/null +++ b/packages/client/lib/commands/ZADD_INCR.ts @@ -0,0 +1,39 @@ +import { RedisArgument, Command } from '../RESP/types'; +import { pushMembers } from './ZADD'; +import { SortedSetMember, transformNullableDoubleReply } from './generic-transformers'; + +export interface ZAddOptions { + condition?: 'NX' | 'XX'; + comparison?: 'LT' | 'GT'; + CH?: boolean; +} + +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, + members: SortedSetMember | Array, + options?: ZAddOptions + ) { + const args = ['ZADD', key]; + + if (options?.condition) { + args.push(options.condition); + } + + if (options?.comparison) { + args.push(options.comparison); + } + + if (options?.CH) { + args.push('CH'); + } + + args.push('INCR'); + + pushMembers(args, members); + + return args; + }, + transformReply: transformNullableDoubleReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZCARD.spec.ts b/packages/client/lib/commands/ZCARD.spec.ts index 2e90da772b6..ff4bb881fb7 100644 --- a/packages/client/lib/commands/ZCARD.spec.ts +++ b/packages/client/lib/commands/ZCARD.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZCARD'; +import ZCARD from './ZCARD'; describe('ZCARD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['ZCARD', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZCARD.transformArguments('key'), + ['ZCARD', 'key'] + ); + }); - testUtils.testWithClient('client.zCard', async client => { - assert.equal( - await client.zCard('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zCard', async client => { + assert.equal( + await client.zCard('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZCARD.ts b/packages/client/lib/commands/ZCARD.ts index f208c181369..c11cb69db3c 100644 --- a/packages/client/lib/commands/ZCARD.ts +++ b/packages/client/lib/commands/ZCARD.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['ZCARD', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZCOUNT.spec.ts b/packages/client/lib/commands/ZCOUNT.spec.ts index e185ed3cd45..357f24bd796 100644 --- a/packages/client/lib/commands/ZCOUNT.spec.ts +++ b/packages/client/lib/commands/ZCOUNT.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZCOUNT'; +import ZCOUNT from './ZCOUNT'; describe('ZCOUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 1), - ['ZCOUNT', 'key', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZCOUNT.transformArguments('key', 0, 1), + ['ZCOUNT', 'key', '0', '1'] + ); + }); - testUtils.testWithClient('client.zCount', async client => { - assert.equal( - await client.zCount('key', 0, 1), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zCount', async client => { + assert.equal( + await client.zCount('key', 0, 1), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZCOUNT.ts b/packages/client/lib/commands/ZCOUNT.ts index f9700cc9099..187a316b15a 100644 --- a/packages/client/lib/commands/ZCOUNT.ts +++ b/packages/client/lib/commands/ZCOUNT.ts @@ -1,21 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { transformStringDoubleArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - min: RedisCommandArgument | number, - max: RedisCommandArgument | number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + min: number | RedisArgument, + max: number | RedisArgument + ) { return [ - 'ZCOUNT', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) + 'ZCOUNT', + key, + transformStringDoubleArgument(min), + transformStringDoubleArgument(max) ]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZDIFF.spec.ts b/packages/client/lib/commands/ZDIFF.spec.ts index 8bb1a101f53..a285190398c 100644 --- a/packages/client/lib/commands/ZDIFF.spec.ts +++ b/packages/client/lib/commands/ZDIFF.spec.ts @@ -1,30 +1,33 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZDIFF'; +import ZDIFF from './ZDIFF'; describe('ZDIFF', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['ZDIFF', '1', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + ZDIFF.transformArguments('key'), + ['ZDIFF', '1', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZDIFF', '2', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + ZDIFF.transformArguments(['1', '2']), + ['ZDIFF', '2', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.zDiff', async client => { - assert.deepEqual( - await client.zDiff('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zDiff', async client => { + assert.deepEqual( + await client.zDiff('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZDIFF.ts b/packages/client/lib/commands/ZDIFF.ts index f3818a139f1..f16c8717cdb 100644 --- a/packages/client/lib/commands/ZDIFF.ts +++ b/packages/client/lib/commands/ZDIFF.ts @@ -1,14 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -export function transformArguments( - keys: Array | RedisCommandArgument -): RedisCommandArguments { - return pushVerdictArgument(['ZDIFF'], keys); -} - -export declare function transformReply(): Array; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; + +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments(keys: RedisVariadicArgument) { + return pushVariadicArgument(['ZDIFF'], keys); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZDIFFSTORE.spec.ts b/packages/client/lib/commands/ZDIFFSTORE.spec.ts index c63902b2666..1bd779302bf 100644 --- a/packages/client/lib/commands/ZDIFFSTORE.spec.ts +++ b/packages/client/lib/commands/ZDIFFSTORE.spec.ts @@ -1,30 +1,33 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZDIFFSTORE'; +import ZDIFFSTORE from './ZDIFFSTORE'; describe('ZDIFFSTORE', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['ZDIFFSTORE', 'destination', '1', 'key'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + ZDIFFSTORE.transformArguments('destination', 'key'), + ['ZDIFFSTORE', 'destination', '1', 'key'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['ZDIFFSTORE', 'destination', '2', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + ZDIFFSTORE.transformArguments('destination', ['1', '2']), + ['ZDIFFSTORE', 'destination', '2', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.zDiffStore', async client => { - assert.equal( - await client.zDiffStore('destination', 'key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zDiffStore', async client => { + assert.equal( + await client.zDiffStore('{tag}destination', '{tag}key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZDIFFSTORE.ts b/packages/client/lib/commands/ZDIFFSTORE.ts index 3b9af9511c5..e4614a1cb14 100644 --- a/packages/client/lib/commands/ZDIFFSTORE.ts +++ b/packages/client/lib/commands/ZDIFFSTORE.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - destination: RedisCommandArgument, - keys: Array | RedisCommandArgument -): RedisCommandArguments { - return pushVerdictArgument(['ZDIFFSTORE', destination], keys); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments( + destination: RedisArgument, + inputKeys: RedisVariadicArgument + ) { + return pushVariadicArgument(['ZDIFFSTORE', destination], inputKeys); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts b/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts index 3b9cb725aaa..4fcd1f978a3 100644 --- a/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts @@ -1,30 +1,33 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZDIFF_WITHSCORES'; +import ZDIFF_WITHSCORES from './ZDIFF_WITHSCORES'; describe('ZDIFF WITHSCORES', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key'), - ['ZDIFF', '1', 'key', 'WITHSCORES'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + ZDIFF_WITHSCORES.transformArguments('key'), + ['ZDIFF', '1', 'key', 'WITHSCORES'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZDIFF', '2', '1', '2', 'WITHSCORES'] - ); - }); + it('array', () => { + assert.deepEqual( + ZDIFF_WITHSCORES.transformArguments(['1', '2']), + ['ZDIFF', '2', '1', '2', 'WITHSCORES'] + ); }); + }); - testUtils.testWithClient('client.zDiffWithScores', async client => { - assert.deepEqual( - await client.zDiffWithScores('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zDiffWithScores', async client => { + assert.deepEqual( + await client.zDiffWithScores('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts index 9caa13c9f8b..f971e828574 100644 --- a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts +++ b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts @@ -1,13 +1,14 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformZDiffArguments } from './ZDIFF'; +import { Command } from '../RESP/types'; +import ZDIFF from './ZDIFF'; +import { transformSortedSetReply } from './generic-transformers'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZDIFF'; - -export function transformArguments(...args: Parameters): RedisCommandArguments { - return [ - ...transformZDiffArguments(...args), - 'WITHSCORES' - ]; -} - -export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: ZDIFF.FIRST_KEY_INDEX, + IS_READ_ONLY: ZDIFF.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = ZDIFF.transformArguments(...args); + redisArgs.push('WITHSCORES'); + return redisArgs; + }, + transformReply: transformSortedSetReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZINCRBY.spec.ts b/packages/client/lib/commands/ZINCRBY.spec.ts index bf2a34b0965..fea3c7a2f71 100644 --- a/packages/client/lib/commands/ZINCRBY.spec.ts +++ b/packages/client/lib/commands/ZINCRBY.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZINCRBY'; +import ZINCRBY from './ZINCRBY'; describe('ZINCRBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1, 'member'), - ['ZINCRBY', 'key', '1', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZINCRBY.transformArguments('key', 1, 'member'), + ['ZINCRBY', 'key', '1', 'member'] + ); + }); - testUtils.testWithClient('client.zIncrBy', async client => { - assert.equal( - await client.zIncrBy('destination', 1, 'member'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zIncrBy', async client => { + assert.equal( + await client.zIncrBy('destination', 1, 'member'), + 1 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZINCRBY.ts b/packages/client/lib/commands/ZINCRBY.ts index 68d89351391..d9e43845016 100644 --- a/packages/client/lib/commands/ZINCRBY.ts +++ b/packages/client/lib/commands/ZINCRBY.ts @@ -1,19 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformNumberInfinityArgument } from './generic-transformers'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformDoubleArgument, transformDoubleReply } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + transformArguments( + key: RedisArgument, increment: number, - member: RedisCommandArgument -): RedisCommandArguments { + member: RedisArgument + ) { return [ - 'ZINCRBY', - key, - transformNumberInfinityArgument(increment), - member + 'ZINCRBY', + key, + transformDoubleArgument(increment), + member ]; -} - -export { transformNumberInfinityReply as transformReply } from './generic-transformers'; + }, + transformReply: transformDoubleReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZINTER.spec.ts b/packages/client/lib/commands/ZINTER.spec.ts index 4d2d86c8869..0d678ce1151 100644 --- a/packages/client/lib/commands/ZINTER.spec.ts +++ b/packages/client/lib/commands/ZINTER.spec.ts @@ -1,58 +1,65 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZINTER'; +import ZINTER from './ZINTER'; describe('ZINTER', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - describe('transformArguments', () => { - it('key (string)', () => { - assert.deepEqual( - transformArguments('key'), - ['ZINTER', '1', 'key'] - ); - }); + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + ZINTER.transformArguments('key'), + ['ZINTER', '1', 'key'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZINTER', '2', '1', '2'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + ZINTER.transformArguments(['1', '2']), + ['ZINTER', '2', '1', '2'] + ); + }); - it('with WEIGHTS', () => { - assert.deepEqual( - transformArguments('key', { - WEIGHTS: [1] - }), - ['ZINTER', '1', 'key', 'WEIGHTS', '1'] - ); - }); + it('key & weight', () => { + assert.deepEqual( + ZINTER.transformArguments({ + key: 'key', + weight: 1 + }), + ['ZINTER', '1', 'key', 'WEIGHTS', '1'] + ); + }); - it('with AGGREGATE', () => { - assert.deepEqual( - transformArguments('key', { - AGGREGATE: 'SUM' - }), - ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM'] - ); - }); + it('keys & weights', () => { + assert.deepEqual( + ZINTER.transformArguments([{ + key: 'a', + weight: 1 + }, { + key: 'b', + weight: 2 + }]), + ['ZINTER', '2', 'a', 'b', 'WEIGHTS', '1', '2'] + ); + }); - it('with WEIGHTS, AGGREGATE', () => { - assert.deepEqual( - transformArguments('key', { - WEIGHTS: [1], - AGGREGATE: 'SUM' - }), - ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM'] - ); - }); + it('with AGGREGATE', () => { + assert.deepEqual( + ZINTER.transformArguments('key', { + AGGREGATE: 'SUM' + }), + ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM'] + ); }); + }); - testUtils.testWithClient('client.zInter', async client => { - assert.deepEqual( - await client.zInter('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zInter', async client => { + assert.deepEqual( + await client.zInter('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZINTER.ts b/packages/client/lib/commands/ZINTER.ts index 88d7f801882..392c3a96c65 100644 --- a/packages/client/lib/commands/ZINTER.ts +++ b/packages/client/lib/commands/ZINTER.ts @@ -1,33 +1,39 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { ZKeys, pushZKeysArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 2; +export type ZInterKeyAndWeight = { + key: RedisArgument; + weight: number; +}; -export const IS_READ_ONLY = true; +export type ZInterKeys = T | [T, ...Array]; -interface ZInterOptions { - WEIGHTS?: Array; - AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; +export interface ZInterOptions { + AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; } -export function transformArguments( - keys: Array | RedisCommandArgument, - options?: ZInterOptions -): RedisCommandArguments { - const args = pushVerdictArgument(['ZINTER'], keys); - - if (options?.WEIGHTS) { - args.push( - 'WEIGHTS', - ...options.WEIGHTS.map(weight => weight.toString()) - ); - } +export function pushZInterArguments( + args: Array, + keys: ZKeys, + options?: ZInterOptions +) { + args = pushZKeysArguments(args, keys); - if (options?.AGGREGATE) { - args.push('AGGREGATE', options.AGGREGATE); - } + if (options?.AGGREGATE) { + args.push('AGGREGATE', options.AGGREGATE); + } - return args; + return args; } -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments( + keys: ZInterKeys | ZInterKeys, + options?: ZInterOptions + ) { + return pushZInterArguments(['ZINTER'], keys, options); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZINTERCARD.spec.ts b/packages/client/lib/commands/ZINTERCARD.spec.ts index 492c1a90433..312e2825ff4 100644 --- a/packages/client/lib/commands/ZINTERCARD.spec.ts +++ b/packages/client/lib/commands/ZINTERCARD.spec.ts @@ -1,30 +1,44 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZINTERCARD'; +import ZINTERCARD from './ZINTERCARD'; describe('ZINTERCARD', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZINTERCARD', '2', '1', '2'] - ); - }); - - it('with limit', () => { - assert.deepEqual( - transformArguments(['1', '2'], 1), - ['ZINTERCARD', '2', '1', '2', 'LIMIT', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ZINTERCARD.transformArguments(['1', '2']), + ['ZINTERCARD', '2', '1', '2'] + ); }); - testUtils.testWithClient('client.zInterCard', async client => { + describe('with LIMIT', () => { + it('plain number (backwards compatibility)', () => { + assert.deepEqual( + ZINTERCARD.transformArguments(['1', '2'], 1), + ['ZINTERCARD', '2', '1', '2', 'LIMIT', '1'] + ); + }); + + it('{ LIMIT: number }', () => { assert.deepEqual( - await client.zInterCard('key'), - 0 + ZINTERCARD.transformArguments(['1', '2'], { + LIMIT: 1 + }), + ['ZINTERCARD', '2', '1', '2', 'LIMIT', '1'] ); - }, GLOBAL.SERVERS.OPEN); + }); + }); + }); + + testUtils.testAll('zInterCard', async client => { + assert.deepEqual( + await client.zInterCard('key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZINTERCARD.ts b/packages/client/lib/commands/ZINTERCARD.ts index ff45ab2aa01..9953d88eecc 100644 --- a/packages/client/lib/commands/ZINTERCARD.ts +++ b/packages/client/lib/commands/ZINTERCARD.ts @@ -1,21 +1,27 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; +export interface ZInterCardOptions { + LIMIT?: number; +} -export function transformArguments( - keys: Array | RedisCommandArgument, - limit?: number -): RedisCommandArguments { - const args = pushVerdictArgument(['ZINTERCARD'], keys); +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments( + keys: RedisVariadicArgument, + options?: ZInterCardOptions['LIMIT'] | ZInterCardOptions + ) { + const args = pushVariadicArgument(['ZINTERCARD'], keys); - if (limit) { - args.push('LIMIT', limit.toString()); + // backwards compatibility + if (typeof options === 'number') { + args.push('LIMIT', options.toString()); + } else if (options?.LIMIT) { + args.push('LIMIT', options.LIMIT.toString()); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZINTERSTORE.spec.ts b/packages/client/lib/commands/ZINTERSTORE.spec.ts index 224961f0786..27914ca0327 100644 --- a/packages/client/lib/commands/ZINTERSTORE.spec.ts +++ b/packages/client/lib/commands/ZINTERSTORE.spec.ts @@ -1,56 +1,63 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZINTERSTORE'; +import ZINTERSTORE from './ZINTERSTORE'; describe('ZINTERSTORE', () => { - describe('transformArguments', () => { - it('key (string)', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['ZINTERSTORE', 'destination', '1', 'key'] - ); - }); + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + ZINTERSTORE.transformArguments('destination', 'source'), + ['ZINTERSTORE', 'destination', '1', 'source'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['ZINTERSTORE', 'destination', '2', '1', '2'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + ZINTERSTORE.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('key & weight', () => { + assert.deepEqual( + ZINTERSTORE.transformArguments('destination', { + key: 'source', + weight: 1 + }), + ['ZINTERSTORE', 'destination', '1', 'source', 'WEIGHTS', '1'] + ); + }); - it('with AGGREGATE', () => { - assert.deepEqual( - transformArguments('destination', 'key', { - AGGREGATE: 'SUM' - }), - ['ZINTERSTORE', 'destination', '1', 'key', 'AGGREGATE', 'SUM'] - ); - }); + it('keys & weights', () => { + assert.deepEqual( + ZINTERSTORE.transformArguments('destination', [{ + key: 'a', + weight: 1 + }, { + key: 'b', + weight: 2 + }]), + ['ZINTERSTORE', 'destination', '2', 'a', 'b', 'WEIGHTS', '1', '2'] + ); + }); - it('with WEIGHTS, AGGREGATE', () => { - assert.deepEqual( - transformArguments('destination', 'key', { - WEIGHTS: [1], - AGGREGATE: 'SUM' - }), - ['ZINTERSTORE', 'destination', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM'] - ); - }); + it('with AGGREGATE', () => { + assert.deepEqual( + ZINTERSTORE.transformArguments('destination', 'source', { + AGGREGATE: 'SUM' + }), + ['ZINTERSTORE', 'destination', '1', 'source', 'AGGREGATE', 'SUM'] + ); }); + }); - testUtils.testWithClient('client.zInterStore', async client => { - assert.equal( - await client.zInterStore('destination', 'key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zInterStore', async client => { + assert.equal( + await client.zInterStore('{tag}destination', '{tag}key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZINTERSTORE.ts b/packages/client/lib/commands/ZINTERSTORE.ts index 540f10ae2d8..a5334566d73 100644 --- a/packages/client/lib/commands/ZINTERSTORE.ts +++ b/packages/client/lib/commands/ZINTERSTORE.ts @@ -1,32 +1,17 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { pushZInterArguments, ZInterOptions } from './ZINTER'; +import { ZKeys } from './generic-transformers'; -interface ZInterStoreOptions { - WEIGHTS?: Array; - AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; -} - -export function transformArguments( - destination: RedisCommandArgument, - keys: Array | RedisCommandArgument, - options?: ZInterStoreOptions -): RedisCommandArguments { - 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 declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + destination: RedisArgument, + keys: ZKeys, + options?: ZInterOptions + ) { + return pushZInterArguments(['ZINTERSTORE', destination], keys, options); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts b/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts index 0eaeb26a244..05790510e49 100644 --- a/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts @@ -1,58 +1,65 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZINTER_WITHSCORES'; +import ZINTER_WITHSCORES from './ZINTER_WITHSCORES'; describe('ZINTER WITHSCORES', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - describe('transformArguments', () => { - it('key (string)', () => { - assert.deepEqual( - transformArguments('key'), - ['ZINTER', '1', 'key', 'WITHSCORES'] - ); - }); + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + ZINTER_WITHSCORES.transformArguments('key'), + ['ZINTER', '1', 'key', 'WITHSCORES'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZINTER', '2', '1', '2', 'WITHSCORES'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + ZINTER_WITHSCORES.transformArguments(['1', '2']), + ['ZINTER', '2', '1', '2', 'WITHSCORES'] + ); + }); - it('with WEIGHTS', () => { - assert.deepEqual( - transformArguments('key', { - WEIGHTS: [1] - }), - ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES'] - ); - }); + it('key & weight', () => { + assert.deepEqual( + ZINTER_WITHSCORES.transformArguments({ + key: 'key', + weight: 1 + }), + ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES'] + ); + }); - it('with AGGREGATE', () => { - assert.deepEqual( - transformArguments('key', { - AGGREGATE: 'SUM' - }), - ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES'] - ); - }); + it('keys & weights', () => { + assert.deepEqual( + ZINTER_WITHSCORES.transformArguments([{ + key: 'a', + weight: 1 + }, { + key: 'b', + weight: 2 + }]), + ['ZINTER', '2', 'a', 'b', 'WEIGHTS', '1', '2', 'WITHSCORES'] + ); + }); - it('with WEIGHTS, AGGREGATE', () => { - assert.deepEqual( - transformArguments('key', { - WEIGHTS: [1], - AGGREGATE: 'SUM' - }), - ['ZINTER', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM', 'WITHSCORES'] - ); - }); + it('with AGGREGATE', () => { + assert.deepEqual( + ZINTER_WITHSCORES.transformArguments('key', { + AGGREGATE: 'SUM' + }), + ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES'] + ); }); + }); - testUtils.testWithClient('client.zInterWithScores', async client => { - assert.deepEqual( - await client.zInterWithScores('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zInterWithScores', async client => { + assert.deepEqual( + await client.zInterWithScores('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZINTER_WITHSCORES.ts b/packages/client/lib/commands/ZINTER_WITHSCORES.ts index c9416e9222a..b287649fbc0 100644 --- a/packages/client/lib/commands/ZINTER_WITHSCORES.ts +++ b/packages/client/lib/commands/ZINTER_WITHSCORES.ts @@ -1,13 +1,14 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformZInterArguments } from './ZINTER'; +import { Command } from '../RESP/types'; +import ZINTER from './ZINTER'; +import { transformSortedSetReply } from './generic-transformers'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZINTER'; - -export function transformArguments(...args: Parameters): RedisCommandArguments { - return [ - ...transformZInterArguments(...args), - 'WITHSCORES' - ]; -} - -export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: ZINTER.FIRST_KEY_INDEX, + IS_READ_ONLY: ZINTER.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = ZINTER.transformArguments(...args); + redisArgs.push('WITHSCORES'); + return redisArgs; + }, + transformReply: transformSortedSetReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZLEXCOUNT.spec.ts b/packages/client/lib/commands/ZLEXCOUNT.spec.ts index 85809f1a9a9..d0a76075579 100644 --- a/packages/client/lib/commands/ZLEXCOUNT.spec.ts +++ b/packages/client/lib/commands/ZLEXCOUNT.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZLEXCOUNT'; +import ZLEXCOUNT from './ZLEXCOUNT'; describe('ZLEXCOUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '[a', '[b'), - ['ZLEXCOUNT', 'key', '[a', '[b'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZLEXCOUNT.transformArguments('key', '[a', '[b'), + ['ZLEXCOUNT', 'key', '[a', '[b'] + ); + }); - testUtils.testWithClient('client.zLexCount', async client => { - assert.equal( - await client.zLexCount('key', '[a', '[b'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zLexCount', async client => { + assert.equal( + await client.zLexCount('key', '[a', '[b'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZLEXCOUNT.ts b/packages/client/lib/commands/ZLEXCOUNT.ts index e2fbcdbb42b..26c9e0d70ac 100644 --- a/packages/client/lib/commands/ZLEXCOUNT.ts +++ b/packages/client/lib/commands/ZLEXCOUNT.ts @@ -1,20 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - min: RedisCommandArgument, - max: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + min: RedisArgument, + max: RedisArgument + ) { return [ - 'ZLEXCOUNT', - key, - min, - max + 'ZLEXCOUNT', + key, + min, + max ]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZMPOP.spec.ts b/packages/client/lib/commands/ZMPOP.spec.ts index 9a0c9676c20..6335fca81cb 100644 --- a/packages/client/lib/commands/ZMPOP.spec.ts +++ b/packages/client/lib/commands/ZMPOP.spec.ts @@ -1,32 +1,55 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZMPOP'; +import ZMPOP from './ZMPOP'; describe('ZMPOP', () => { - testUtils.isVersionGreaterThanHook([7]); + testUtils.isVersionGreaterThanHook([7]); - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', 'MIN'), - ['ZMPOP', '1', 'key', 'MIN'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ZMPOP.transformArguments('key', 'MIN'), + ['ZMPOP', '1', 'key', 'MIN'] + ); + }); - it('with score and count', () => { - assert.deepEqual( - transformArguments('key', 'MIN', { - COUNT: 2 - }), - ['ZMPOP', '1', 'key', 'MIN', 'COUNT', '2'] - ); - }); + it('with count', () => { + assert.deepEqual( + ZMPOP.transformArguments('key', 'MIN', { + COUNT: 2 + }), + ['ZMPOP', '1', 'key', 'MIN', 'COUNT', '2'] + ); }); + }); + + testUtils.testAll('zmPop - null', async client => { + assert.equal( + await client.zmPop('key', 'MIN'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testAll('zmPop - with members', async client => { + const members = [{ + value: '1', + score: 1 + }]; - testUtils.testWithClient('client.zmPop', async client => { - assert.deepEqual( - await client.zmPop('key', 'MIN'), - null - ); - }, GLOBAL.SERVERS.OPEN); + const [, reply] = await Promise.all([ + client.zAdd('key', members), + client.zmPop('key', 'MIN') + ]); + + assert.deepEqual(reply, { + key: 'key', + members + }); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZMPOP.ts b/packages/client/lib/commands/ZMPOP.ts index 0baa46bbf0b..57d2cccdaca 100644 --- a/packages/client/lib/commands/ZMPOP.ts +++ b/packages/client/lib/commands/ZMPOP.ts @@ -1,34 +1,61 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { SortedSetSide, transformSortedSetMemberReply, transformZMPopArguments, ZMember, ZMPopOptions } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 2; - -export function transformArguments( - keys: RedisCommandArgument | Array, - side: SortedSetSide, - options?: ZMPopOptions -): RedisCommandArguments { - return transformZMPopArguments( - ['ZMPOP'], - keys, - side, - options - ); +import { RedisArgument, NullReply, TuplesReply, BlobStringReply, DoubleReply, ArrayReply, UnwrapReply, Resp2Reply, Command, TypeMapping } from '../RESP/types'; +import { pushVariadicArgument, RedisVariadicArgument, SortedSetSide, transformSortedSetReply, transformDoubleReply } from './generic-transformers'; + +export interface ZMPopOptions { + COUNT?: number; } -type ZMPopRawReply = null | [ - key: string, - elements: Array<[RedisCommandArgument, RedisCommandArgument]> -]; +export type ZMPopRawReply = NullReply | TuplesReply<[ + key: BlobStringReply, + members: ArrayReply> +]>; -type ZMPopReply = null | { - key: string, - elements: Array -}; +export function transformZMPopArguments( + args: Array, + keys: RedisVariadicArgument, + side: SortedSetSide, + options?: ZMPopOptions +) { + args = pushVariadicArgument(args, keys); -export function transformReply(reply: ZMPopRawReply): ZMPopReply { - return reply === null ? null : { - key: reply[0], - elements: reply[1].map(transformSortedSetMemberReply) - }; + args.push(side); + + if (options?.COUNT) { + args.push('COUNT', options.COUNT.toString()); + } + + return args; } + +export type ZMPopArguments = typeof transformZMPopArguments extends (_: any, ...args: infer T) => any ? T : never; + +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments(...args: ZMPopArguments) { + return transformZMPopArguments(['ZMPOP'], ...args); + }, + transformReply: { + 2(reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) { + return reply === null ? null : { + key: reply[0], + members: (reply[1] as unknown as UnwrapReply).map(member => { + const [value, score] = member as unknown as UnwrapReply; + return { + value, + score: transformDoubleReply[2](score, preserve, typeMapping) + }; + }) + }; + }, + 3(reply: UnwrapReply) { + return reply === null ? null : { + key: reply[0], + members: transformSortedSetReply[3](reply[1]) + }; + } + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZMSCORE.spec.ts b/packages/client/lib/commands/ZMSCORE.spec.ts index 228c8e9d6f6..5035724b117 100644 --- a/packages/client/lib/commands/ZMSCORE.spec.ts +++ b/packages/client/lib/commands/ZMSCORE.spec.ts @@ -1,30 +1,33 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZMSCORE'; +import ZMSCORE from './ZMSCORE'; describe('ZMSCORE', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['ZMSCORE', 'key', 'member'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + ZMSCORE.transformArguments('key', 'member'), + ['ZMSCORE', 'key', 'member'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['ZMSCORE', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + ZMSCORE.transformArguments('key', ['1', '2']), + ['ZMSCORE', 'key', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.zmScore', async client => { - assert.deepEqual( - await client.zmScore('key', 'member'), - [null] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zmScore', async client => { + assert.deepEqual( + await client.zmScore('key', 'member'), + [null] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZMSCORE.ts b/packages/client/lib/commands/ZMSCORE.ts index 6c8c9dace31..00ade13b011 100644 --- a/packages/client/lib/commands/ZMSCORE.ts +++ b/packages/client/lib/commands/ZMSCORE.ts @@ -1,15 +1,19 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, ArrayReply, NullReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { createTransformNullableDoubleReplyResp2Func, pushVariadicArguments, RedisVariadicArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['ZMSCORE', key], member); -} - -export { transformNumberInfinityNullArrayReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + member: RedisVariadicArgument + ) { + return pushVariadicArguments(['ZMSCORE', key], member); + }, + transformReply: { + 2: (reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { + return reply.map(createTransformNullableDoubleReplyResp2Func(preserve, typeMapping)); + }, + 3: undefined as unknown as () => ArrayReply + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZPOPMAX.spec.ts b/packages/client/lib/commands/ZPOPMAX.spec.ts index 18fba23a3e9..609ccb826b5 100644 --- a/packages/client/lib/commands/ZPOPMAX.spec.ts +++ b/packages/client/lib/commands/ZPOPMAX.spec.ts @@ -1,41 +1,39 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './ZPOPMAX'; +import ZPOPMAX from './ZPOPMAX'; describe('ZPOPMAX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['ZPOPMAX', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZPOPMAX.transformArguments('key'), + ['ZPOPMAX', 'key'] + ); + }); - it('transformReply', () => { - assert.deepEqual( - transformReply(['value', '1']), - { - value: 'value', - score: 1 - } - ); - }); + testUtils.testAll('zPopMax - null', async client => { + assert.equal( + await client.zPopMax('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); - describe('client.zPopMax', () => { - testUtils.testWithClient('null', async client => { - assert.equal( - await client.zPopMax('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zPopMax - with member', async client => { + const member = { + value: 'value', + score: 1 + }; - testUtils.testWithClient('member', async client => { - const member = { score: 1, value: 'value' }, - [, zPopMaxReply] = await Promise.all([ - client.zAdd('key', member), - client.zPopMax('key') - ]); + const [, reply] = await Promise.all([ + client.zAdd('key', member), + client.zPopMax('key') + ]); - assert.deepEqual(zPopMaxReply, member); - }, GLOBAL.SERVERS.OPEN); - }); + assert.deepEqual(reply, member); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZPOPMAX.ts b/packages/client/lib/commands/ZPOPMAX.ts index 811166a690c..130309347a6 100644 --- a/packages/client/lib/commands/ZPOPMAX.ts +++ b/packages/client/lib/commands/ZPOPMAX.ts @@ -1,12 +1,28 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { transformDoubleReply } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument) { + return ['ZPOPMAX', key]; + }, + transformReply: { + 2: (reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { + if (reply.length === 0) return null; -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return [ - 'ZPOPMAX', - key - ]; -} + return { + value: reply[0], + score: transformDoubleReply[2](reply[1], preserve, typeMapping), + }; + }, + 3: (reply: UnwrapReply>) => { + if (reply.length === 0) return null; -export { transformSortedSetMemberNullReply as transformReply } from './generic-transformers'; + return { + value: reply[0], + score: reply[1] + }; + } + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts b/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts index b282d0d3199..b653b1f3f1a 100644 --- a/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts +++ b/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts @@ -1,19 +1,32 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZPOPMAX_COUNT'; +import ZPOPMAX_COUNT from './ZPOPMAX_COUNT'; describe('ZPOPMAX COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['ZPOPMAX', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZPOPMAX_COUNT.transformArguments('key', 1), + ['ZPOPMAX', 'key', '1'] + ); + }); - testUtils.testWithClient('client.zPopMaxCount', async client => { - assert.deepEqual( - await client.zPopMaxCount('key', 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zPopMaxCount', async client => { + const members = [{ + value: '1', + score: 1 + }, { + value: '2', + score: 2 + }]; + + const [ , reply] = await Promise.all([ + client.zAdd('key', members), + client.zPopMaxCount('key', members.length) + ]); + + assert.deepEqual(reply, members.reverse()); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZPOPMAX_COUNT.ts b/packages/client/lib/commands/ZPOPMAX_COUNT.ts index 875bcfb9147..00d39536ae1 100644 --- a/packages/client/lib/commands/ZPOPMAX_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMAX_COUNT.ts @@ -1,16 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformZPopMaxArguments } from './ZPOPMAX'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformSortedSetReply } from './generic-transformers'; -export { FIRST_KEY_INDEX } from './ZPOPMAX'; - -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { - return [ - ...transformZPopMaxArguments(key), - count.toString() - ]; -} - -export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, count: number) { + return ['ZPOPMAX', key, count.toString()]; + }, + transformReply: transformSortedSetReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZPOPMIN.spec.ts b/packages/client/lib/commands/ZPOPMIN.spec.ts index 624b7054404..0b2c57d28b9 100644 --- a/packages/client/lib/commands/ZPOPMIN.spec.ts +++ b/packages/client/lib/commands/ZPOPMIN.spec.ts @@ -1,41 +1,39 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './ZPOPMIN'; +import ZPOPMIN from './ZPOPMIN'; describe('ZPOPMIN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['ZPOPMIN', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZPOPMIN.transformArguments('key'), + ['ZPOPMIN', 'key'] + ); + }); - it('transformReply', () => { - assert.deepEqual( - transformReply(['value', '1']), - { - value: 'value', - score: 1 - } - ); - }); + testUtils.testAll('zPopMin - null', async client => { + assert.equal( + await client.zPopMin('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); - describe('client.zPopMin', () => { - testUtils.testWithClient('null', async client => { - assert.equal( - await client.zPopMin('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zPopMax - with member', async client => { + const member = { + value: 'value', + score: 1 + }; - testUtils.testWithClient('member', async client => { - const member = { score: 1, value: 'value' }, - [, zPopMinReply] = await Promise.all([ - client.zAdd('key', member), - client.zPopMin('key') - ]); + const [, reply] = await Promise.all([ + client.zAdd('key', member), + client.zPopMin('key') + ]); - assert.deepEqual(zPopMinReply, member); - }, GLOBAL.SERVERS.OPEN); - }); + assert.deepEqual(reply, member); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZPOPMIN.ts b/packages/client/lib/commands/ZPOPMIN.ts index 053ffd2d2ce..b9da85cc974 100644 --- a/packages/client/lib/commands/ZPOPMIN.ts +++ b/packages/client/lib/commands/ZPOPMIN.ts @@ -1,12 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, Command } from '../RESP/types'; +import ZPOPMAX from './ZPOPMAX'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { - return [ - 'ZPOPMIN', - key - ]; -} - -export { transformSortedSetMemberNullReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument) { + return ['ZPOPMIN', key]; + }, + transformReply: ZPOPMAX.transformReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts b/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts index 6d40002ab72..fa3d9e2a975 100644 --- a/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts +++ b/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts @@ -1,19 +1,32 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZPOPMIN_COUNT'; +import ZPOPMIN_COUNT from './ZPOPMIN_COUNT'; describe('ZPOPMIN COUNT', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['ZPOPMIN', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZPOPMIN_COUNT.transformArguments('key', 1), + ['ZPOPMIN', 'key', '1'] + ); + }); - testUtils.testWithClient('client.zPopMinCount', async client => { - assert.deepEqual( - await client.zPopMinCount('key', 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zPopMinCount', async client => { + const members = [{ + value: '1', + score: 1 + }, { + value: '2', + score: 2 + }]; + + const [ , reply] = await Promise.all([ + client.zAdd('key', members), + client.zPopMinCount('key', members.length) + ]); + + assert.deepEqual(reply, members); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.SERVERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZPOPMIN_COUNT.ts b/packages/client/lib/commands/ZPOPMIN_COUNT.ts index 54125ade0ac..2433686da56 100644 --- a/packages/client/lib/commands/ZPOPMIN_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMIN_COUNT.ts @@ -1,16 +1,11 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformZPopMinArguments } from './ZPOPMIN'; +import { RedisArgument, Command } from '../RESP/types'; +import { transformSortedSetReply } from './generic-transformers'; -export { FIRST_KEY_INDEX } from './ZPOPMIN'; - -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { - return [ - ...transformZPopMinArguments(key), - count.toString() - ]; -} - -export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, count: number) { + return ['ZPOPMIN', key, count.toString()]; + }, + transformReply: transformSortedSetReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANDMEMBER.spec.ts b/packages/client/lib/commands/ZRANDMEMBER.spec.ts index c57d26f830e..519850f5eff 100644 --- a/packages/client/lib/commands/ZRANDMEMBER.spec.ts +++ b/packages/client/lib/commands/ZRANDMEMBER.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZRANDMEMBER'; +import ZRANDMEMBER from './ZRANDMEMBER'; describe('ZRANDMEMBER', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['ZRANDMEMBER', 'key'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZRANDMEMBER.transformArguments('key'), + ['ZRANDMEMBER', 'key'] + ); + }); - testUtils.testWithClient('client.zRandMember', async client => { - assert.equal( - await client.zRandMember('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRandMember', async client => { + assert.equal( + await client.zRandMember('key'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZRANDMEMBER.ts b/packages/client/lib/commands/ZRANDMEMBER.ts index 00420872c0c..449eb281c66 100644 --- a/packages/client/lib/commands/ZRANDMEMBER.ts +++ b/packages/client/lib/commands/ZRANDMEMBER.ts @@ -1,11 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(key: RedisCommandArgument): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['ZRANDMEMBER', key]; -} - -export declare function transformReply(): RedisCommandArgument | null; + }, + transformReply: undefined as unknown as () => BlobStringReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts index 10db0727b23..2d0f4b9ced8 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZRANDMEMBER_COUNT'; +import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; describe('ZRANDMEMBER COUNT', () => { - testUtils.isVersionGreaterThanHook([6, 2, 5]); + testUtils.isVersionGreaterThanHook([6, 2, 5]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['ZRANDMEMBER', 'key', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZRANDMEMBER_COUNT.transformArguments('key', 1), + ['ZRANDMEMBER', 'key', '1'] + ); + }); - testUtils.testWithClient('client.zRandMemberCount', async client => { - assert.deepEqual( - await client.zRandMemberCount('key', 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRandMemberCount', async client => { + assert.deepEqual( + await client.zRandMemberCount('key', 1), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts index 3aa91902c62..89b921f007a 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts @@ -1,16 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformArguments as transformZRandMemberArguments } from './ZRANDMEMBER'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import ZRANDMEMBER from './ZRANDMEMBER'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANDMEMBER'; - -export function transformArguments( - key: RedisCommandArgument, - count: number -): RedisCommandArguments { - return [ - ...transformZRandMemberArguments(key), - count.toString() - ]; -} - -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: ZRANDMEMBER.FIRST_KEY_INDEX, + IS_READ_ONLY: ZRANDMEMBER.IS_READ_ONLY, + transformArguments(key: RedisArgument, count: number) { + const args = ZRANDMEMBER.transformArguments(key); + args.push(count.toString()); + return args; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts index 5b5ec1f500f..aeeea3f6e71 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts @@ -1,21 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZRANDMEMBER_COUNT_WITHSCORES'; +import ZRANDMEMBER_COUNT_WITHSCORES from './ZRANDMEMBER_COUNT_WITHSCORES'; describe('ZRANDMEMBER COUNT WITHSCORES', () => { - testUtils.isVersionGreaterThanHook([6, 2, 5]); + testUtils.isVersionGreaterThanHook([6, 2, 5]); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 1), - ['ZRANDMEMBER', 'key', '1', 'WITHSCORES'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZRANDMEMBER_COUNT_WITHSCORES.transformArguments('key', 1), + ['ZRANDMEMBER', 'key', '1', 'WITHSCORES'] + ); + }); - testUtils.testWithClient('client.zRandMemberCountWithScores', async client => { - assert.deepEqual( - await client.zRandMemberCountWithScores('key', 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRandMemberCountWithScores', async client => { + assert.deepEqual( + await client.zRandMemberCountWithScores('key', 1), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts index cc9d2bc26ee..14c28d4b6c6 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts @@ -1,13 +1,14 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformZRandMemberCountArguments } from './ZRANDMEMBER_COUNT'; +import { Command, RedisArgument } from '../RESP/types'; +import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; +import { transformSortedSetReply } from './generic-transformers'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANDMEMBER_COUNT'; - -export function transformArguments(...args: Parameters): RedisCommandArguments { - return [ - ...transformZRandMemberCountArguments(...args), - 'WITHSCORES' - ]; -} - -export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: ZRANDMEMBER_COUNT.FIRST_KEY_INDEX, + IS_READ_ONLY: ZRANDMEMBER_COUNT.IS_READ_ONLY, + transformArguments(key: RedisArgument, count: number) { + const args = ZRANDMEMBER_COUNT.transformArguments(key, count); + args.push('WITHSCORES'); + return args; + }, + transformReply: transformSortedSetReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGE.spec.ts b/packages/client/lib/commands/ZRANGE.spec.ts index a280aff0033..db940062b2f 100644 --- a/packages/client/lib/commands/ZRANGE.spec.ts +++ b/packages/client/lib/commands/ZRANGE.spec.ts @@ -1,74 +1,77 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZRANGE'; +import ZRANGE from './ZRANGE'; describe('ZRANGE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', 0, 1), - ['ZRANGE', 'src', '0', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ZRANGE.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 BYSCORE', () => { + assert.deepEqual( + ZRANGE.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 BYLEX', () => { + assert.deepEqual( + ZRANGE.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 REV', () => { + assert.deepEqual( + ZRANGE.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 LIMIT', () => { + assert.deepEqual( + ZRANGE.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'] - ); - }); + it('with BY & REV & LIMIT', () => { + assert.deepEqual( + ZRANGE.transformArguments('src', 0, 1, { + BY: 'SCORE', + REV: true, + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1'] + ); }); + }); - testUtils.testWithClient('client.zRange', async client => { - assert.deepEqual( - await client.zRange('src', 0, 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRange', async client => { + assert.deepEqual( + await client.zRange('src', 0, 1), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZRANGE.ts b/packages/client/lib/commands/ZRANGE.ts index 83f09aaa1b0..557044b67da 100644 --- a/packages/client/lib/commands/ZRANGE.ts +++ b/packages/client/lib/commands/ZRANGE.ts @@ -1,51 +1,54 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } 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; - }; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { transformStringDoubleArgument } from './generic-transformers'; + +export interface ZRangeOptions { + BY?: 'SCORE' | 'LEX'; + REV?: boolean; + LIMIT?: { + offset: number; + count: number; + }; } -export function transformArguments( - key: RedisCommandArgument, - min: RedisCommandArgument | number, - max: RedisCommandArgument | number, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + min: RedisArgument | number, + max: RedisArgument | number, options?: ZRangeOptions -): RedisCommandArguments { + ) { const args = [ - 'ZRANGE', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) + 'ZRANGE', + key, + transformStringDoubleArgument(min), + transformStringDoubleArgument(max) ]; switch (options?.BY) { - case 'SCORE': - args.push('BYSCORE'); - break; + case 'SCORE': + args.push('BYSCORE'); + break; - case 'LEX': - args.push('BYLEX'); - break; + case 'LEX': + args.push('BYLEX'); + break; } if (options?.REV) { - args.push('REV'); + args.push('REV'); } if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); + args.push( + 'LIMIT', + options.LIMIT.offset.toString(), + options.LIMIT.count.toString() + ); } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGEBYLEX.spec.ts b/packages/client/lib/commands/ZRANGEBYLEX.spec.ts index fe7b7d5a16e..f3f6f4bc0e5 100644 --- a/packages/client/lib/commands/ZRANGEBYLEX.spec.ts +++ b/packages/client/lib/commands/ZRANGEBYLEX.spec.ts @@ -1,33 +1,36 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZRANGEBYLEX'; +import ZRANGEBYLEX from './ZRANGEBYLEX'; describe('ZRANGEBYLEX', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', '-', '+'), - ['ZRANGEBYLEX', 'src', '-', '+'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ZRANGEBYLEX.transformArguments('src', '-', '+'), + ['ZRANGEBYLEX', 'src', '-', '+'] + ); + }); - it('with LIMIT', () => { - assert.deepEqual( - transformArguments('src', '-', '+', { - LIMIT: { - offset: 0, - count: 1 - } - }), - ['ZRANGEBYLEX', 'src', '-', '+', 'LIMIT', '0', '1'] - ); - }); + it('with LIMIT', () => { + assert.deepEqual( + ZRANGEBYLEX.transformArguments('src', '-', '+', { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGEBYLEX', 'src', '-', '+', 'LIMIT', '0', '1'] + ); }); + }); - testUtils.testWithClient('client.zRangeByLex', async client => { - assert.deepEqual( - await client.zRangeByLex('src', '-', '+'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRangeByLex', async client => { + assert.deepEqual( + await client.zRangeByLex('src', '-', '+'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZRANGEBYLEX.ts b/packages/client/lib/commands/ZRANGEBYLEX.ts index d6e621a562f..afe7718f3c3 100644 --- a/packages/client/lib/commands/ZRANGEBYLEX.ts +++ b/packages/client/lib/commands/ZRANGEBYLEX.ts @@ -1,35 +1,34 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { transformStringDoubleArgument } from './generic-transformers'; export interface ZRangeByLexOptions { - LIMIT?: { - offset: number; - count: number; - }; + LIMIT?: { + offset: number; + count: number; + }; } -export function transformArguments( - key: RedisCommandArgument, - min: RedisCommandArgument, - max: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + min: RedisArgument, + max: RedisArgument, options?: ZRangeByLexOptions -): RedisCommandArguments { + ) { const args = [ - 'ZRANGEBYLEX', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) + 'ZRANGEBYLEX', + key, + transformStringDoubleArgument(min), + transformStringDoubleArgument(max) ]; if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); + args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts b/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts index a3484326306..61267ea7f2f 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts @@ -1,33 +1,36 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZRANGEBYSCORE'; +import ZRANGEBYSCORE from './ZRANGEBYSCORE'; describe('ZRANGEBYSCORE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', 0, 1), - ['ZRANGEBYSCORE', 'src', '0', '1'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ZRANGEBYSCORE.transformArguments('src', 0, 1), + ['ZRANGEBYSCORE', 'src', '0', '1'] + ); + }); - it('with LIMIT', () => { - assert.deepEqual( - transformArguments('src', 0, 1, { - LIMIT: { - offset: 0, - count: 1 - } - }), - ['ZRANGEBYSCORE', 'src', '0', '1', 'LIMIT', '0', '1'] - ); - }); + it('with LIMIT', () => { + assert.deepEqual( + ZRANGEBYSCORE.transformArguments('src', 0, 1, { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGEBYSCORE', 'src', '0', '1', 'LIMIT', '0', '1'] + ); }); + }); - testUtils.testWithClient('client.zRangeByScore', async client => { - assert.deepEqual( - await client.zRangeByScore('src', 0, 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRangeByScore', async client => { + assert.deepEqual( + await client.zRangeByScore('src', 0, 1), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZRANGEBYSCORE.ts b/packages/client/lib/commands/ZRANGEBYSCORE.ts index 5ab7d7ac727..e54c96380de 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE.ts @@ -1,35 +1,36 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { transformStringDoubleArgument } from './generic-transformers'; export interface ZRangeByScoreOptions { - LIMIT?: { - offset: number; - count: number; - }; + LIMIT?: { + offset: number; + count: number; + }; } -export function transformArguments( - key: RedisCommandArgument, +export declare function transformReply(): Array; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, min: string | number, max: string | number, options?: ZRangeByScoreOptions -): RedisCommandArguments { + ) { const args = [ - 'ZRANGEBYSCORE', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) + 'ZRANGEBYSCORE', + key, + transformStringDoubleArgument(min), + transformStringDoubleArgument(max) ]; if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); + args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts index 3552d3e2535..e70a97b0372 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts @@ -1,33 +1,36 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZRANGEBYSCORE_WITHSCORES'; +import ZRANGEBYSCORE_WITHSCORES from './ZRANGEBYSCORE_WITHSCORES'; describe('ZRANGEBYSCORE WITHSCORES', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', 0, 1), - ['ZRANGEBYSCORE', 'src', '0', '1', 'WITHSCORES'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ZRANGEBYSCORE_WITHSCORES.transformArguments('src', 0, 1), + ['ZRANGEBYSCORE', 'src', '0', '1', 'WITHSCORES'] + ); + }); - it('with LIMIT', () => { - assert.deepEqual( - transformArguments('src', 0, 1, { - LIMIT: { - offset: 0, - count: 1 - } - }), - ['ZRANGEBYSCORE', 'src', '0', '1', 'LIMIT', '0', '1', 'WITHSCORES'] - ); - }); + it('with LIMIT', () => { + assert.deepEqual( + ZRANGEBYSCORE_WITHSCORES.transformArguments('src', 0, 1, { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGEBYSCORE', 'src', '0', '1', 'LIMIT', '0', '1', 'WITHSCORES'] + ); }); + }); - testUtils.testWithClient('client.zRangeByScoreWithScores', async client => { - assert.deepEqual( - await client.zRangeByScoreWithScores('src', 0, 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRangeByScoreWithScores', async client => { + assert.deepEqual( + await client.zRangeByScoreWithScores('src', 0, 1), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts index c7266f1c062..bfbe09c6e26 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts @@ -1,18 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ZRangeByScoreOptions, transformArguments as transformZRangeByScoreArguments } from './ZRANGEBYSCORE'; +import { Command } from '../RESP/types'; +import ZRANGEBYSCORE from './ZRANGEBYSCORE'; +import { transformSortedSetReply } from './generic-transformers'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANGEBYSCORE'; - -export function transformArguments( - key: RedisCommandArgument, - min: string | number, - max: string | number, - options?: ZRangeByScoreOptions -): RedisCommandArguments { - return [ - ...transformZRangeByScoreArguments(key, min, max, options), - 'WITHSCORES' - ]; -} - -export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: ZRANGEBYSCORE.FIRST_KEY_INDEX, + IS_READ_ONLY: ZRANGEBYSCORE.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = ZRANGEBYSCORE.transformArguments(...args); + redisArgs.push('WITHSCORES'); + return redisArgs; + }, + transformReply: transformSortedSetReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGESTORE.spec.ts b/packages/client/lib/commands/ZRANGESTORE.spec.ts index 7af253e539f..51315d3463b 100644 --- a/packages/client/lib/commands/ZRANGESTORE.spec.ts +++ b/packages/client/lib/commands/ZRANGESTORE.spec.ts @@ -1,92 +1,81 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './ZRANGESTORE'; +import ZRANGESTORE from './ZRANGESTORE'; describe('ZRANGESTORE', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([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'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ZRANGESTORE.transformArguments('destination', 'source', 0, 1), + ['ZRANGESTORE', 'destination', 'source', '0', '1'] + ); + }); - it('with BYLEX', () => { - assert.deepEqual( - transformArguments('dst', 'src', 0, 1, { - BY: 'LEX' - }), - ['ZRANGESTORE', 'dst', 'src', '0', '1', 'BYLEX'] - ); - }); + it('with BYSCORE', () => { + assert.deepEqual( + ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + BY: 'SCORE' + }), + ['ZRANGESTORE', 'destination', 'source', '0', '1', 'BYSCORE'] + ); + }); - it('with REV', () => { - assert.deepEqual( - transformArguments('dst', 'src', 0, 1, { - REV: true - }), - ['ZRANGESTORE', 'dst', 'src', '0', '1', 'REV'] - ); - }); + it('with BYLEX', () => { + assert.deepEqual( + ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + BY: 'LEX' + }), + ['ZRANGESTORE', 'destination', 'source', '0', '1', 'BYLEX'] + ); + }); - 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 REV', () => { + assert.deepEqual( + ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + REV: true + }), + ['ZRANGESTORE', 'destination', 'source', '0', '1', 'REV'] + ); + }); - 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'] - ); - }); + it('with LIMIT', () => { + assert.deepEqual( + ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGESTORE', 'destination', 'source', '0', '1', 'LIMIT', '0', '1'] + ); }); - describe('transformReply', () => { - it('should throw TypeError when reply is not a number', () => { - assert.throws( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - () => (transformReply as any)([]), - TypeError - ); - }); + it('with BY & REV & LIMIT', () => { + assert.deepEqual( + ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + BY: 'SCORE', + REV: true, + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGESTORE', 'destination', 'source', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1'] + ); }); + }); - testUtils.testWithClient('client.zRangeStore', async client => { - await client.zAdd('src', { - score: 0.5, - value: 'value' - }); + testUtils.testWithClient('client.zRangeStore', async client => { + const [, reply] = await Promise.all([ + client.zAdd('{tag}source', { + score: 1, + value: '1' + }), + client.zRangeStore('{tag}destination', '{tag}source', 0, 1) + ]); - assert.equal( - await client.zRangeStore('dst', 'src', 0, 1), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 1); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ZRANGESTORE.ts b/packages/client/lib/commands/ZRANGESTORE.ts index 28067ceabe0..96f10120b87 100644 --- a/packages/client/lib/commands/ZRANGESTORE.ts +++ b/packages/client/lib/commands/ZRANGESTORE.ts @@ -1,62 +1,52 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; - -export const FIRST_KEY_INDEX = 1; - -interface ZRangeStoreOptions { - BY?: 'SCORE' | 'LEX'; - REV?: true; - LIMIT?: { - offset: number; - count: number; - }; - WITHSCORES?: true; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { transformStringDoubleArgument } from './generic-transformers'; + +export interface ZRangeStoreOptions { + BY?: 'SCORE' | 'LEX'; + REV?: true; + LIMIT?: { + offset: number; + count: number; + }; } -export function transformArguments( - dst: RedisCommandArgument, - src: RedisCommandArgument, - min: RedisCommandArgument | number, - max: RedisCommandArgument | number, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + destination: RedisArgument, + source: RedisArgument, + min: RedisArgument | number, + max: RedisArgument | number, options?: ZRangeStoreOptions -): RedisCommandArguments { + ) { const args = [ - 'ZRANGESTORE', - dst, - src, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) + 'ZRANGESTORE', + destination, + source, + transformStringDoubleArgument(min), + transformStringDoubleArgument(max) ]; switch (options?.BY) { - case 'SCORE': - args.push('BYSCORE'); - break; + case 'SCORE': + args.push('BYSCORE'); + break; - case 'LEX': - args.push('BYLEX'); - break; + case 'LEX': + args.push('BYLEX'); + break; } if (options?.REV) { - args.push('REV'); + args.push('REV'); } if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); - } - - if (options?.WITHSCORES) { - args.push('WITHSCORES'); + args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); } 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; -} + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts b/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts index d9b07e19dda..038c150a675 100644 --- a/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts @@ -1,65 +1,75 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZRANGE_WITHSCORES'; +import ZRANGE_WITHSCORES from './ZRANGE_WITHSCORES'; describe('ZRANGE WITHSCORES', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('src', 0, 1), - ['ZRANGE', 'src', '0', '1', 'WITHSCORES'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ZRANGE_WITHSCORES.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 BY', () => { + assert.deepEqual( + ZRANGE_WITHSCORES.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 REV', () => { + assert.deepEqual( + ZRANGE_WITHSCORES.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 LIMIT', () => { + assert.deepEqual( + ZRANGE_WITHSCORES.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'] - ); - }); + it('with BY & REV & LIMIT', () => { + assert.deepEqual( + ZRANGE_WITHSCORES.transformArguments('src', 0, 1, { + BY: 'SCORE', + REV: true, + LIMIT: { + offset: 0, + count: 1 + } + }), + ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'REV', 'LIMIT', '0', '1', 'WITHSCORES'] + ); }); + }); + + testUtils.testAll('zRangeWithScores', async client => { + const members = [{ + value: '1', + score: 1 + }]; + + const [, reply] = await Promise.all([ + client.zAdd('key', members), + client.zRangeWithScores('key', 0, 1) + ]); - testUtils.testWithClient('client.zRangeWithScores', async client => { - assert.deepEqual( - await client.zRangeWithScores('src', 0, 1), - [] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, members); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts index 23ea4d6337c..cfa90e99ea4 100644 --- a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts @@ -1,13 +1,15 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformZRangeArguments } from './ZRANGE'; +import { Command } from '../RESP/types'; +import ZRANGE from './ZRANGE'; +import { transformSortedSetReply } from './generic-transformers'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZRANGE'; +export default { + FIRST_KEY_INDEX: ZRANGE.FIRST_KEY_INDEX, + IS_READ_ONLY: ZRANGE.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = ZRANGE.transformArguments(...args); + redisArgs.push('WITHSCORES'); + return redisArgs; + }, + transformReply: transformSortedSetReply +} as const satisfies Command; -export function transformArguments(...args: Parameters): RedisCommandArguments { - return [ - ...transformZRangeArguments(...args), - 'WITHSCORES' - ]; -} - -export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers'; diff --git a/packages/client/lib/commands/ZRANK.spec.ts b/packages/client/lib/commands/ZRANK.spec.ts index 0c81517a7d6..9341709bda3 100644 --- a/packages/client/lib/commands/ZRANK.spec.ts +++ b/packages/client/lib/commands/ZRANK.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZRANK'; +import ZRANK from './ZRANK'; describe('ZRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['ZRANK', 'key', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZRANK.transformArguments('key', 'member'), + ['ZRANK', 'key', 'member'] + ); + }); - testUtils.testWithClient('client.zRank', async client => { - assert.equal( - await client.zRank('key', 'member'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRank', async client => { + assert.equal( + await client.zRank('key', 'member'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZRANK.ts b/packages/client/lib/commands/ZRANK.ts index 33439ea4b55..11184c0a28f 100644 --- a/packages/client/lib/commands/ZRANK.ts +++ b/packages/client/lib/commands/ZRANK.ts @@ -1,14 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, member: RedisArgument) { return ['ZRANK', key, member]; -} - -export declare function transformReply(): number | null; + }, + transformReply: undefined as unknown as () => NumberReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts new file mode 100644 index 00000000000..b571e0f7071 --- /dev/null +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts @@ -0,0 +1,46 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ZRANK_WITHSCORE from './ZRANK_WITHSCORE'; + +describe('ZRANK WITHSCORE', () => { + testUtils.isVersionGreaterThanHook([7, 2]); + + it('transformArguments', () => { + assert.deepEqual( + ZRANK_WITHSCORE.transformArguments('key', 'member'), + ['ZRANK', 'key', 'member', 'WITHSCORE'] + ); + }); + + testUtils.testAll('zRankWithScore - null', async client => { + assert.equal( + await client.zRankWithScore('key', 'member'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); + + testUtils.testAll('zRankWithScore - with member', async client => { + const member = { + value: '1', + score: 1 + } + + const [, reply] = await Promise.all([ + client.zAdd('key', member), + client.zRankWithScore('key', member.value) + ]) + assert.deepEqual( + reply, + { + rank: 0, + score: 1 + } + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); +}); diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.ts new file mode 100644 index 00000000000..39c788535e3 --- /dev/null +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.ts @@ -0,0 +1,30 @@ +import { NullReply, TuplesReply, NumberReply, BlobStringReply, DoubleReply, UnwrapReply, Command } from '../RESP/types'; +import ZRANK from './ZRANK'; + +export default { + FIRST_KEY_INDEX: ZRANK.FIRST_KEY_INDEX, + IS_READ_ONLY: ZRANK.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = ZRANK.transformArguments(...args); + redisArgs.push('WITHSCORE'); + return redisArgs; + }, + transformReply: { + 2: (reply: UnwrapReply>) => { + if (reply === null) return null; + + return { + rank: reply[0], + score: Number(reply[1]) + }; + }, + 3: (reply: UnwrapReply>) => { + if (reply === null) return null; + + return { + rank: reply[0], + score: reply[1] + }; + } + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZREM.spec.ts b/packages/client/lib/commands/ZREM.spec.ts index 3ac001708a0..4b203c9f4eb 100644 --- a/packages/client/lib/commands/ZREM.spec.ts +++ b/packages/client/lib/commands/ZREM.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZREM'; +import ZREM from './ZREM'; describe('ZREM', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['ZREM', 'key', 'member'] - ); - }); + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + ZREM.transformArguments('key', 'member'), + ['ZREM', 'key', 'member'] + ); + }); - it('array', () => { - assert.deepEqual( - transformArguments('key', ['1', '2']), - ['ZREM', 'key', '1', '2'] - ); - }); + it('array', () => { + assert.deepEqual( + ZREM.transformArguments('key', ['1', '2']), + ['ZREM', 'key', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.zRem', async client => { - assert.equal( - await client.zRem('key', 'member'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRem', async client => { + assert.equal( + await client.zRem('key', 'member'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZREM.ts b/packages/client/lib/commands/ZREM.ts index 7ab92c4a78f..54f55841fce 100644 --- a/packages/client/lib/commands/ZREM.ts +++ b/packages/client/lib/commands/ZREM.ts @@ -1,13 +1,14 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArguments } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument | Array -): RedisCommandArguments { - return pushVerdictArguments(['ZREM', key], member); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + member: RedisVariadicArgument + ) { + return pushVariadicArguments(['ZREM', key], member); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts b/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts index b59c9e9f3b0..9f29c3cdcf7 100644 --- a/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts +++ b/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZREMRANGEBYLEX'; +import ZREMRANGEBYLEX from './ZREMRANGEBYLEX'; describe('ZREMRANGEBYLEX', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '[a', '[b'), - ['ZREMRANGEBYLEX', 'key', '[a', '[b'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZREMRANGEBYLEX.transformArguments('key', '[a', '[b'), + ['ZREMRANGEBYLEX', 'key', '[a', '[b'] + ); + }); - testUtils.testWithClient('client.zRemRangeByLex', async client => { - assert.equal( - await client.zRemRangeByLex('key', '[a', '[b'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRemRangeByLex', async client => { + assert.equal( + await client.zRemRangeByLex('key', '[a', '[b'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZREMRANGEBYLEX.ts b/packages/client/lib/commands/ZREMRANGEBYLEX.ts index f1f3908f538..e3cd7013ac2 100644 --- a/packages/client/lib/commands/ZREMRANGEBYLEX.ts +++ b/packages/client/lib/commands/ZREMRANGEBYLEX.ts @@ -1,19 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; +import { NumberReply, Command, RedisArgument } from '../RESP/types'; +import { transformStringDoubleArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - min: RedisCommandArgument | number, - max: RedisCommandArgument | number -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + min: RedisArgument | number, + max: RedisArgument | number + ) { return [ - 'ZREMRANGEBYLEX', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) + 'ZREMRANGEBYLEX', + key, + transformStringDoubleArgument(min), + transformStringDoubleArgument(max) ]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts b/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts index c659dadb790..12627083e1a 100644 --- a/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts +++ b/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZREMRANGEBYRANK'; +import ZREMRANGEBYRANK from './ZREMRANGEBYRANK'; describe('ZREMRANGEBYRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 1), - ['ZREMRANGEBYRANK', 'key', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZREMRANGEBYRANK.transformArguments('key', 0, 1), + ['ZREMRANGEBYRANK', 'key', '0', '1'] + ); + }); - testUtils.testWithClient('client.zRemRangeByRank', async client => { - assert.equal( - await client.zRemRangeByRank('key', 0, 1), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRemRangeByRank', async client => { + assert.equal( + await client.zRemRangeByRank('key', 0, 1), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZREMRANGEBYRANK.ts b/packages/client/lib/commands/ZREMRANGEBYRANK.ts index c50d06e3bf6..986de33060e 100644 --- a/packages/client/lib/commands/ZREMRANGEBYRANK.ts +++ b/packages/client/lib/commands/ZREMRANGEBYRANK.ts @@ -1,13 +1,13 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, start: number, - stop: number -): RedisCommandArguments { + stop: number) { return ['ZREMRANGEBYRANK', key, start.toString(), stop.toString()]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts b/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts index 988fd7690c9..fb3ba4e718c 100644 --- a/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts +++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZREMRANGEBYSCORE'; +import ZREMRANGEBYSCORE from './ZREMRANGEBYSCORE'; describe('ZREMRANGEBYSCORE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 0, 1), - ['ZREMRANGEBYSCORE', 'key', '0', '1'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZREMRANGEBYSCORE.transformArguments('key', 0, 1), + ['ZREMRANGEBYSCORE', 'key', '0', '1'] + ); + }); - testUtils.testWithClient('client.zRemRangeByScore', async client => { - assert.equal( - await client.zRemRangeByScore('key', 0, 1), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRemRangeByScore', async client => { + assert.equal( + await client.zRemRangeByScore('key', 0, 1), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts index 12d1eff811e..7050f2627a7 100644 --- a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts +++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts @@ -1,19 +1,20 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { transformStringNumberInfinityArgument } from './generic-transformers'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { transformStringDoubleArgument } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - key: RedisCommandArgument, - min: RedisCommandArgument | number, - max: RedisCommandArgument | number, -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + min: RedisArgument | number, + max: RedisArgument | number, + ) { return [ - 'ZREMRANGEBYSCORE', - key, - transformStringNumberInfinityArgument(min), - transformStringNumberInfinityArgument(max) + 'ZREMRANGEBYSCORE', + key, + transformStringDoubleArgument(min), + transformStringDoubleArgument(max) ]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZREVRANK.spec.ts b/packages/client/lib/commands/ZREVRANK.spec.ts index d9fef0d70a4..418773b6003 100644 --- a/packages/client/lib/commands/ZREVRANK.spec.ts +++ b/packages/client/lib/commands/ZREVRANK.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZREVRANK'; +import ZREVRANK from './ZREVRANK'; describe('ZREVRANK', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['ZREVRANK', 'key', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZREVRANK.transformArguments('key', 'member'), + ['ZREVRANK', 'key', 'member'] + ); + }); - testUtils.testWithClient('client.zRevRank', async client => { - assert.equal( - await client.zRevRank('key', 'member'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zRevRank', async client => { + assert.equal( + await client.zRevRank('key', 'member'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZREVRANK.ts b/packages/client/lib/commands/ZREVRANK.ts index b88936c0c92..3bf52d21de5 100644 --- a/packages/client/lib/commands/ZREVRANK.ts +++ b/packages/client/lib/commands/ZREVRANK.ts @@ -1,14 +1,10 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { NumberReply, NullReply, Command, RedisArgument } from '../RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, member: RedisArgument) { return ['ZREVRANK', key, member]; -} - -export declare function transformReply(): number | null; + }, + transformReply: undefined as unknown as () => NumberReply | NullReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZSCAN.spec.ts b/packages/client/lib/commands/ZSCAN.spec.ts index afa221a1ef3..ebeaad2a4d0 100644 --- a/packages/client/lib/commands/ZSCAN.spec.ts +++ b/packages/client/lib/commands/ZSCAN.spec.ts @@ -1,77 +1,52 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './ZSCAN'; +import ZSCAN 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'] - ); - }); + describe('transformArguments', () => { + it('cusror only', () => { + assert.deepEqual( + ZSCAN.transformArguments('key', '0'), + ['ZSCAN', 'key', '0'] + ); + }); - it('with MATCH & COUNT', () => { - assert.deepEqual( - transformArguments('key', 0, { - MATCH: 'pattern', - COUNT: 1 - }), - ['ZSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] - ); - }); + it('with MATCH', () => { + assert.deepEqual( + ZSCAN.transformArguments('key', '0', { + MATCH: 'pattern' + }), + ['ZSCAN', 'key', '0', 'MATCH', 'pattern'] + ); }); - describe('transformReply', () => { - it('without members', () => { - assert.deepEqual( - transformReply(['0', []]), - { - cursor: 0, - members: [] - } - ); - }); + it('with COUNT', () => { + assert.deepEqual( + ZSCAN.transformArguments('key', '0', { + COUNT: 1 + }), + ['ZSCAN', 'key', '0', 'COUNT', '1'] + ); + }); - it('with members', () => { - assert.deepEqual( - transformReply(['0', ['member', '-inf']]), - { - cursor: 0, - members: [{ - value: 'member', - score: -Infinity - }] - } - ); - }); + it('with MATCH & COUNT', () => { + assert.deepEqual( + ZSCAN.transformArguments('key', '0', { + MATCH: 'pattern', + COUNT: 1 + }), + ['ZSCAN', 'key', '0', 'MATCH', 'pattern', 'COUNT', '1'] + ); }); + }); - testUtils.testWithClient('client.zScan', async client => { - assert.deepEqual( - await client.zScan('key', 0), - { - cursor: 0, - members: [] - } - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('zScan', async client => { + assert.deepEqual( + await client.zScan('key', '0'), + { + cursor: '0', + members: [] + } + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/client/lib/commands/ZSCAN.ts b/packages/client/lib/commands/ZSCAN.ts index f6fa17c2d4e..853cdf098f6 100644 --- a/packages/client/lib/commands/ZSCAN.ts +++ b/packages/client/lib/commands/ZSCAN.ts @@ -1,39 +1,26 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { ScanOptions, transformNumberInfinityReply, pushScanArguments, ZMember } from './generic-transformers'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { ScanCommonOptions, pushScanArguments } from './SCAN'; +import { transformSortedSetReply } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - cursor: number, - options?: ScanOptions -): RedisCommandArguments { - return pushScanArguments([ - 'ZSCAN', - key - ], cursor, options); +export interface HScanEntry { + field: BlobStringReply; + value: BlobStringReply; } -type ZScanRawReply = [RedisCommandArgument, Array]; - -interface ZScanReply { - cursor: number; - members: Array; -} - -export function transformReply([cursor, rawMembers]: ZScanRawReply): ZScanReply { - const parsedMembers: Array = []; - for (let i = 0; i < rawMembers.length; i += 2) { - parsedMembers.push({ - value: rawMembers[i], - score: transformNumberInfinityReply(rawMembers[i + 1]) - }); - } - +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + cursor: RedisArgument, + options?: ScanCommonOptions + ) { + return pushScanArguments(['ZSCAN', key], cursor, options); + }, + transformReply([cursor, rawMembers]: [BlobStringReply, ArrayReply]) { return { - cursor: Number(cursor), - members: parsedMembers + cursor, + members: transformSortedSetReply[2](rawMembers) }; -} + } +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZSCORE.spec.ts b/packages/client/lib/commands/ZSCORE.spec.ts index fe2a1c6a7c5..3d8df6640c8 100644 --- a/packages/client/lib/commands/ZSCORE.spec.ts +++ b/packages/client/lib/commands/ZSCORE.spec.ts @@ -1,19 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZSCORE'; +import ZSCORE from './ZSCORE'; describe('ZSCORE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'member'), - ['ZSCORE', 'key', 'member'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + ZSCORE.transformArguments('key', 'member'), + ['ZSCORE', 'key', 'member'] + ); + }); - testUtils.testWithClient('client.zScore', async client => { - assert.equal( - await client.zScore('key', 'member'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zScore', async client => { + assert.equal( + await client.zScore('key', 'member'), + null + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZSCORE.ts b/packages/client/lib/commands/ZSCORE.ts index 118abc10850..0d3db752e1c 100644 --- a/packages/client/lib/commands/ZSCORE.ts +++ b/packages/client/lib/commands/ZSCORE.ts @@ -1,14 +1,12 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '../RESP/types'; +import { transformNullableDoubleReply } from './generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments( - key: RedisCommandArgument, - member: RedisCommandArgument -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, member: RedisArgument) { return ['ZSCORE', key, member]; -} - -export { transformNumberInfinityNullReply as transformReply } from './generic-transformers'; + }, + transformReply: transformNullableDoubleReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZUNION.spec.ts b/packages/client/lib/commands/ZUNION.spec.ts index c53498cbf65..f66bb7abc63 100644 --- a/packages/client/lib/commands/ZUNION.spec.ts +++ b/packages/client/lib/commands/ZUNION.spec.ts @@ -1,48 +1,65 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZUNION'; +import ZUNION from './ZUNION'; describe('ZUNION', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - describe('transformArguments', () => { - it('key (string)', () => { - assert.deepEqual( - transformArguments('key'), - ['ZUNION', '1', 'key'] - ); - }); + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + ZUNION.transformArguments('key'), + ['ZUNION', '1', 'key'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZUNION', '2', '1', '2'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + ZUNION.transformArguments(['1', '2']), + ['ZUNION', '2', '1', '2'] + ); + }); - it('with WEIGHTS', () => { - assert.deepEqual( - transformArguments('key', { - WEIGHTS: [1] - }), - ['ZUNION', '1', 'key', 'WEIGHTS', '1'] - ); - }); + it('key & weight', () => { + assert.deepEqual( + ZUNION.transformArguments({ + key: 'key', + weight: 1 + }), + ['ZUNION', '1', 'key', 'WEIGHTS', '1'] + ); + }); + + it('keys & weights', () => { + assert.deepEqual( + ZUNION.transformArguments([{ + key: 'a', + weight: 1 + }, { + key: 'b', + weight: 2 + }]), + ['ZUNION', '2', 'a', 'b', 'WEIGHTS', '1', '2'] + ); + }); - it('with AGGREGATE', () => { - assert.deepEqual( - transformArguments('key', { - AGGREGATE: 'SUM' - }), - ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM'] - ); - }); + it('with AGGREGATE', () => { + assert.deepEqual( + ZUNION.transformArguments('key', { + AGGREGATE: 'SUM' + }), + ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM'] + ); }); + }); - testUtils.testWithClient('client.zUnion', async client => { - assert.deepEqual( - await client.zUnion('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zUnion', async client => { + assert.deepEqual( + await client.zUnion('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZUNION.ts b/packages/client/lib/commands/ZUNION.ts index f329348cc8b..09614b9dc01 100644 --- a/packages/client/lib/commands/ZUNION.ts +++ b/packages/client/lib/commands/ZUNION.ts @@ -1,30 +1,24 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { ZKeys, pushZKeysArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 2; - -export const IS_READ_ONLY = true; - -interface ZUnionOptions { - WEIGHTS?: Array; - AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; +export interface ZUnionOptions { + AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; } -export function transformArguments( - keys: Array | RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: true, + transformArguments( + keys: ZKeys, options?: ZUnionOptions -): RedisCommandArguments { - const args = pushVerdictArgument(['ZUNION'], keys); - - if (options?.WEIGHTS) { - args.push('WEIGHTS', ...options.WEIGHTS.map(weight => weight.toString())); - } + ) { + const args = pushZKeysArguments(['ZUNION'], keys); if (options?.AGGREGATE) { - args.push('AGGREGATE', options.AGGREGATE); + args.push('AGGREGATE', options.AGGREGATE); } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZUNIONSTORE.spec.ts b/packages/client/lib/commands/ZUNIONSTORE.spec.ts index 8f11828b221..7a01e80f0c0 100644 --- a/packages/client/lib/commands/ZUNIONSTORE.spec.ts +++ b/packages/client/lib/commands/ZUNIONSTORE.spec.ts @@ -1,56 +1,63 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZUNIONSTORE'; +import ZUNIONSTORE from './ZUNIONSTORE'; describe('ZUNIONSTORE', () => { - describe('transformArguments', () => { - it('key (string)', () => { - assert.deepEqual( - transformArguments('destination', 'key'), - ['ZUNIONSTORE', 'destination', '1', 'key'] - ); - }); + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + ZUNIONSTORE.transformArguments('destination', 'source'), + ['ZUNIONSTORE', 'destination', '1', 'source'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments('destination', ['1', '2']), - ['ZUNIONSTORE', 'destination', '2', '1', '2'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + ZUNIONSTORE.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('key & weight', () => { + assert.deepEqual( + ZUNIONSTORE.transformArguments('destination', { + key: 'source', + weight: 1 + }), + ['ZUNIONSTORE', 'destination', '1', 'source', 'WEIGHTS', '1'] + ); + }); - it('with AGGREGATE', () => { - assert.deepEqual( - transformArguments('destination', 'key', { - AGGREGATE: 'SUM' - }), - ['ZUNIONSTORE', 'destination', '1', 'key', 'AGGREGATE', 'SUM'] - ); - }); + it('keys & weights', () => { + assert.deepEqual( + ZUNIONSTORE.transformArguments('destination', [{ + key: 'a', + weight: 1 + }, { + key: 'b', + weight: 2 + }]), + ['ZUNIONSTORE', 'destination', '2', 'a', 'b', 'WEIGHTS', '1', '2'] + ); + }); - it('with WEIGHTS, AGGREGATE', () => { - assert.deepEqual( - transformArguments('destination', 'key', { - WEIGHTS: [1], - AGGREGATE: 'SUM' - }), - ['ZUNIONSTORE', 'destination', '1', 'key', 'WEIGHTS', '1', 'AGGREGATE', 'SUM'] - ); - }); + it('with AGGREGATE', () => { + assert.deepEqual( + ZUNIONSTORE.transformArguments('destination', 'source', { + AGGREGATE: 'SUM' + }), + ['ZUNIONSTORE', 'destination', '1', 'source', 'AGGREGATE', 'SUM'] + ); }); + }); - testUtils.testWithClient('client.zUnionStore', async client => { - assert.equal( - await client.zUnionStore('destination', 'key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zUnionStore', async client => { + assert.equal( + await client.zUnionStore('{tag}destination', '{tag}key'), + 0 + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZUNIONSTORE.ts b/packages/client/lib/commands/ZUNIONSTORE.ts index 2a42e21bc87..a14d3ba31c9 100644 --- a/packages/client/lib/commands/ZUNIONSTORE.ts +++ b/packages/client/lib/commands/ZUNIONSTORE.ts @@ -1,29 +1,25 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; -import { pushVerdictArgument } from './generic-transformers'; +import { RedisArgument, NumberReply, Command, } from '../RESP/types'; +import { ZKeys, pushZKeysArguments } from './generic-transformers'; -export const FIRST_KEY_INDEX = 1; - -interface ZUnionOptions { - WEIGHTS?: Array; - AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; +export interface ZUnionOptions { + AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; } -export function transformArguments( - destination: RedisCommandArgument, - keys: Array | RedisCommandArgument, +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + destination: RedisArgument, + keys: ZKeys, options?: ZUnionOptions -): RedisCommandArguments { - const args = pushVerdictArgument(['ZUNIONSTORE', destination], keys); - - if (options?.WEIGHTS) { - args.push('WEIGHTS', ...options.WEIGHTS.map(weight => weight.toString())); - } + ) { + const args = pushZKeysArguments(['ZUNIONSTORE', destination], keys); if (options?.AGGREGATE) { - args.push('AGGREGATE', options.AGGREGATE); + args.push('AGGREGATE', options.AGGREGATE); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts b/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts index 3786a97963d..bbf3ec676e8 100644 --- a/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts @@ -1,48 +1,65 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ZUNION_WITHSCORES'; +import ZUNION_WITHSCORES from './ZUNION_WITHSCORES'; describe('ZUNION WITHSCORES', () => { - testUtils.isVersionGreaterThanHook([6, 2]); + testUtils.isVersionGreaterThanHook([6, 2]); - describe('transformArguments', () => { - it('key (string)', () => { - assert.deepEqual( - transformArguments('key'), - ['ZUNION', '1', 'key', 'WITHSCORES'] - ); - }); + describe('transformArguments', () => { + it('key (string)', () => { + assert.deepEqual( + ZUNION_WITHSCORES.transformArguments('key'), + ['ZUNION', '1', 'key', 'WITHSCORES'] + ); + }); - it('keys (array)', () => { - assert.deepEqual( - transformArguments(['1', '2']), - ['ZUNION', '2', '1', '2', 'WITHSCORES'] - ); - }); + it('keys (Array)', () => { + assert.deepEqual( + ZUNION_WITHSCORES.transformArguments(['1', '2']), + ['ZUNION', '2', '1', '2', 'WITHSCORES'] + ); + }); - it('with WEIGHTS', () => { - assert.deepEqual( - transformArguments('key', { - WEIGHTS: [1] - }), - ['ZUNION', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES'] - ); - }); + it('key & weight', () => { + assert.deepEqual( + ZUNION_WITHSCORES.transformArguments({ + key: 'key', + weight: 1 + }), + ['ZUNION', '1', 'key', 'WEIGHTS', '1', 'WITHSCORES'] + ); + }); + + it('keys & weights', () => { + assert.deepEqual( + ZUNION_WITHSCORES.transformArguments([{ + key: 'a', + weight: 1 + }, { + key: 'b', + weight: 2 + }]), + ['ZUNION', '2', 'a', 'b', 'WEIGHTS', '1', '2', 'WITHSCORES'] + ); + }); - it('with AGGREGATE', () => { - assert.deepEqual( - transformArguments('key', { - AGGREGATE: 'SUM' - }), - ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES'] - ); - }); + it('with AGGREGATE', () => { + assert.deepEqual( + ZUNION_WITHSCORES.transformArguments('key', { + AGGREGATE: 'SUM' + }), + ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES'] + ); }); + }); - testUtils.testWithClient('client.zUnionWithScores', async client => { - assert.deepEqual( - await client.zUnionWithScores('key'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testAll('zUnionWithScores', async client => { + assert.deepEqual( + await client.zUnionWithScores('key'), + [] + ); + }, { + client: GLOBAL.SERVERS.OPEN, + cluster: GLOBAL.CLUSTERS.OPEN + }); }); diff --git a/packages/client/lib/commands/ZUNION_WITHSCORES.ts b/packages/client/lib/commands/ZUNION_WITHSCORES.ts index 168cc929ac8..d0895a3de76 100644 --- a/packages/client/lib/commands/ZUNION_WITHSCORES.ts +++ b/packages/client/lib/commands/ZUNION_WITHSCORES.ts @@ -1,13 +1,14 @@ -import { RedisCommandArguments } from '.'; -import { transformArguments as transformZUnionArguments } from './ZUNION'; +import { Command } from '../RESP/types'; +import ZUNION from './ZUNION'; +import { transformSortedSetReply } from './generic-transformers'; -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './ZUNION'; - -export function transformArguments(...args: Parameters): RedisCommandArguments { - return [ - ...transformZUnionArguments(...args), - 'WITHSCORES' - ]; -} - -export { transformSortedSetWithScoresReply as transformReply } from './generic-transformers'; +export default { + FIRST_KEY_INDEX: ZUNION.FIRST_KEY_INDEX, + IS_READ_ONLY: ZUNION.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = ZUNION.transformArguments(...args); + redisArgs.push('WITHSCORES'); + return redisArgs; + }, + transformReply: transformSortedSetReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/generic-transformers.spec.ts b/packages/client/lib/commands/generic-transformers.spec.ts index 60caf26eaad..5f990d4e34d 100644 --- a/packages/client/lib/commands/generic-transformers.spec.ts +++ b/packages/client/lib/commands/generic-transformers.spec.ts @@ -1,718 +1,685 @@ -import { strict as assert } from 'assert'; -import { - transformBooleanReply, - transformBooleanArrayReply, - pushScanArguments, - transformNumberInfinityReply, - transformNumberInfinityNullReply, - transformNumberInfinityArgument, - transformStringNumberInfinityArgument, - transformTuplesReply, - transformStreamMessagesReply, - transformStreamMessagesNullReply, - transformStreamsMessagesReply, - transformSortedSetWithScoresReply, - pushGeoCountArgument, - pushGeoSearchArguments, - GeoReplyWith, - transformGeoMembersWithReply, - transformEXAT, - transformPXAT, - pushEvalArguments, - pushVerdictArguments, - pushVerdictNumberArguments, - pushVerdictArgument, - pushOptionalVerdictArgument, - transformCommandReply, - CommandFlags, - CommandCategories, - pushSlotRangesArguments -} from './generic-transformers'; - -describe('Generic Transformers', () => { - describe('transformBooleanReply', () => { - it('0', () => { - assert.equal( - transformBooleanReply(0), - false - ); - }); - - it('1', () => { - assert.equal( - transformBooleanReply(1), - true - ); - }); - }); - - describe('transformBooleanArrayReply', () => { - it('empty array', () => { - assert.deepEqual( - transformBooleanArrayReply([]), - [] - ); - }); - - it('0, 1', () => { - assert.deepEqual( - transformBooleanArrayReply([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('transformNumberInfinityReply', () => { - it('0.5', () => { - assert.equal( - transformNumberInfinityReply('0.5'), - 0.5 - ); - }); - - it('+inf', () => { - assert.equal( - transformNumberInfinityReply('+inf'), - Infinity - ); - }); - - it('-inf', () => { - assert.equal( - transformNumberInfinityReply('-inf'), - -Infinity - ); - }); - }); - - describe('transformNumberInfinityNullReply', () => { - it('null', () => { - assert.equal( - transformNumberInfinityNullReply(null), - null - ); - }); - - it('1', () => { - assert.equal( - transformNumberInfinityNullReply('1'), - 1 - ); - }); - }); - - describe('transformNumberInfinityArgument', () => { - it('0.5', () => { - assert.equal( - transformNumberInfinityArgument(0.5), - '0.5' - ); - }); - - it('Infinity', () => { - assert.equal( - transformNumberInfinityArgument(Infinity), - '+inf' - ); - }); - - it('-Infinity', () => { - assert.equal( - transformNumberInfinityArgument(-Infinity), - '-inf' - ); - }); - }); - - describe('transformStringNumberInfinityArgument', () => { - it("'0.5'", () => { - assert.equal( - transformStringNumberInfinityArgument('0.5'), - '0.5' - ); - }); - - it('0.5', () => { - assert.equal( - transformStringNumberInfinityArgument(0.5), - '0.5' - ); - }); - }); - - it('transformTuplesReply', () => { - assert.deepEqual( - transformTuplesReply(['key1', 'value1', 'key2', 'value2']), - Object.create(null, { - key1: { - value: 'value1', - configurable: true, - enumerable: true - }, - key2: { - value: 'value2', - configurable: true, - enumerable: true - } - }) - ); - }); - - it('transformStreamMessagesReply', () => { - assert.deepEqual( - transformStreamMessagesReply([['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 - } - }) - }] - ); - }); - - it('transformStreamMessagesNullReply', () => { - assert.deepEqual( - transformStreamMessagesNullReply([null, ['0-0', ['0key', '0value']]]), - [null, { - id: '0-0', - message: Object.create(null, { - '0key': { - value: '0value', - configurable: true, - enumerable: true - } - }) - }] - ); - }); - - it('transformStreamMessagesNullReply', () => { - assert.deepEqual( - transformStreamMessagesNullReply([null, ['0-1', ['11key', '11value']]]), - [null, { - id: '0-1', - message: Object.create(null, { - '11key': { - value: '11value', - configurable: true, - enumerable: true - } - }) - }] - ); - }); - - describe('transformStreamsMessagesReply', () => { - it('null', () => { - assert.equal( - transformStreamsMessagesReply(null), - null - ); - }); - - it('with messages', () => { - assert.deepEqual( - transformStreamsMessagesReply([['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('transformSortedSetWithScoresReply', () => { - assert.deepEqual( - transformSortedSetWithScoresReply(['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'] - ); - }); - - describe('with COUNT', () => { - it('number', () => { - assert.deepEqual( - pushGeoCountArgument([], 1), - ['COUNT', '1'] - ); - }); - - describe('object', () => { - it('value', () => { - assert.deepEqual( - pushGeoCountArgument([], { value: 1 }), - ['COUNT', '1'] - ); - }); - - it('value, 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('pushVerdictArguments', () => { - it('string', () => { - assert.deepEqual( - pushVerdictArguments([], 'string'), - ['string'] - ); - }); - - it('array', () => { - assert.deepEqual( - pushVerdictArguments([], ['1', '2']), - ['1', '2'] - ); - }); - }); - - describe('pushVerdictNumberArguments', () => { - it('number', () => { - assert.deepEqual( - pushVerdictNumberArguments([], 0), - ['0'] - ); - }); - - it('array', () => { - assert.deepEqual( - pushVerdictNumberArguments([], [0, 1]), - ['0', '1'] - ); - }); - }); - - 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'] - ); - }); - }); - - it('transformCommandReply', () => { - assert.deepEqual( - transformCommandReply([ - 'ping', - -1, - [CommandFlags.STALE, CommandFlags.FAST], - 0, - 0, - 0, - [CommandCategories.FAST, CommandCategories.CONNECTION] - ]), - { - name: 'ping', - arity: -1, - flags: new Set([CommandFlags.STALE, CommandFlags.FAST]), - firstKeyIndex: 0, - lastKeyIndex: 0, - step: 0, - categories: new Set([CommandCategories.FAST, CommandCategories.CONNECTION]) - } - ); - }); - - describe('pushSlotRangesArguments', () => { - it('single range', () => { - assert.deepEqual( - pushSlotRangesArguments([], { - start: 0, - end: 1 - }), - ['0', '1'] - ); - }); - - it('multiple ranges', () => { - assert.deepEqual( - pushSlotRangesArguments([], [{ - start: 0, - end: 1 - }, { - start: 2, - end: 3 - }]), - ['0', '1', '2', '3'] - ); - }); - }); -}); +// import { strict as assert } from 'node:assert'; +// import { +// transformBooleanReply, +// transformBooleanArrayReply, +// pushScanArguments, +// transformNumberInfinityReply, +// transformNumberInfinityNullReply, +// transformNumberInfinityArgument, +// transformStringNumberInfinityArgument, +// transformTuplesReply, +// transformStreamMessagesReply, +// transformStreamsMessagesReply, +// transformSortedSetWithScoresReply, +// pushGeoCountArgument, +// pushGeoSearchArguments, +// GeoReplyWith, +// transformGeoMembersWithReply, +// transformEXAT, +// transformPXAT, +// pushEvalArguments, +// pushVariadicArguments, +// pushVariadicNumberArguments, +// pushVariadicArgument, +// pushOptionalVariadicArgument, +// transformCommandReply, +// CommandFlags, +// CommandCategories, +// pushSlotRangesArguments +// } from './generic-transformers'; + +// describe('Generic Transformers', () => { +// describe('transformBooleanReply', () => { +// it('0', () => { +// assert.equal( +// transformBooleanReply(0), +// false +// ); +// }); + +// it('1', () => { +// assert.equal( +// transformBooleanReply(1), +// true +// ); +// }); +// }); + +// describe('transformBooleanArrayReply', () => { +// it('empty array', () => { +// assert.deepEqual( +// transformBooleanArrayReply([]), +// [] +// ); +// }); + +// it('0, 1', () => { +// assert.deepEqual( +// transformBooleanArrayReply([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('transformNumberInfinityReply', () => { +// it('0.5', () => { +// assert.equal( +// transformNumberInfinityReply('0.5'), +// 0.5 +// ); +// }); + +// it('+inf', () => { +// assert.equal( +// transformNumberInfinityReply('+inf'), +// Infinity +// ); +// }); + +// it('-inf', () => { +// assert.equal( +// transformNumberInfinityReply('-inf'), +// -Infinity +// ); +// }); +// }); + +// describe('transformNumberInfinityNullReply', () => { +// it('null', () => { +// assert.equal( +// transformNumberInfinityNullReply(null), +// null +// ); +// }); + +// it('1', () => { +// assert.equal( +// transformNumberInfinityNullReply('1'), +// 1 +// ); +// }); +// }); + +// describe('transformNumberInfinityArgument', () => { +// it('0.5', () => { +// assert.equal( +// transformNumberInfinityArgument(0.5), +// '0.5' +// ); +// }); + +// it('Infinity', () => { +// assert.equal( +// transformNumberInfinityArgument(Infinity), +// '+inf' +// ); +// }); + +// it('-Infinity', () => { +// assert.equal( +// transformNumberInfinityArgument(-Infinity), +// '-inf' +// ); +// }); +// }); + +// describe('transformStringNumberInfinityArgument', () => { +// it("'0.5'", () => { +// assert.equal( +// transformStringNumberInfinityArgument('0.5'), +// '0.5' +// ); +// }); + +// it('0.5', () => { +// assert.equal( +// transformStringNumberInfinityArgument(0.5), +// '0.5' +// ); +// }); +// }); + +// it('transformTuplesReply', () => { +// assert.deepEqual( +// transformTuplesReply(['key1', 'value1', 'key2', 'value2']), +// Object.create(null, { +// key1: { +// value: 'value1', +// configurable: true, +// enumerable: true +// }, +// key2: { +// value: 'value2', +// configurable: true, +// enumerable: true +// } +// }) +// ); +// }); + +// it('transformStreamMessagesReply', () => { +// assert.deepEqual( +// transformStreamMessagesReply([['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('transformStreamsMessagesReply', () => { +// it('null', () => { +// assert.equal( +// transformStreamsMessagesReply(null), +// null +// ); +// }); + +// it('with messages', () => { +// assert.deepEqual( +// transformStreamsMessagesReply([['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('transformSortedSetWithScoresReply', () => { +// assert.deepEqual( +// transformSortedSetWithScoresReply(['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'] +// ); +// }); + +// describe('with COUNT', () => { +// it('number', () => { +// assert.deepEqual( +// pushGeoCountArgument([], 1), +// ['COUNT', '1'] +// ); +// }); + +// describe('object', () => { +// it('value', () => { +// assert.deepEqual( +// pushGeoCountArgument([], { value: 1 }), +// ['COUNT', '1'] +// ); +// }); + +// it('value, 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('pushVariadicArguments', () => { +// it('string', () => { +// assert.deepEqual( +// pushVariadicArguments([], 'string'), +// ['string'] +// ); +// }); + +// it('array', () => { +// assert.deepEqual( +// pushVariadicArguments([], ['1', '2']), +// ['1', '2'] +// ); +// }); +// }); + +// describe('pushVariadicNumberArguments', () => { +// it('number', () => { +// assert.deepEqual( +// pushVariadicNumberArguments([], 0), +// ['0'] +// ); +// }); + +// it('array', () => { +// assert.deepEqual( +// pushVariadicNumberArguments([], [0, 1]), +// ['0', '1'] +// ); +// }); +// }); + +// describe('pushVariadicArgument', () => { +// it('string', () => { +// assert.deepEqual( +// pushVariadicArgument([], 'string'), +// ['1', 'string'] +// ); +// }); + +// it('array', () => { +// assert.deepEqual( +// pushVariadicArgument([], ['1', '2']), +// ['2', '1', '2'] +// ); +// }); +// }); + +// describe('pushOptionalVariadicArgument', () => { +// it('undefined', () => { +// assert.deepEqual( +// pushOptionalVariadicArgument([], 'name', undefined), +// [] +// ); +// }); + +// it('string', () => { +// assert.deepEqual( +// pushOptionalVariadicArgument([], 'name', 'string'), +// ['name', '1', 'string'] +// ); +// }); + +// it('array', () => { +// assert.deepEqual( +// pushOptionalVariadicArgument([], 'name', ['1', '2']), +// ['name', '2', '1', '2'] +// ); +// }); +// }); + +// it('transformCommandReply', () => { +// assert.deepEqual( +// transformCommandReply([ +// 'ping', +// -1, +// [CommandFlags.STALE, CommandFlags.FAST], +// 0, +// 0, +// 0, +// [CommandCategories.FAST, CommandCategories.CONNECTION] +// ]), +// { +// name: 'ping', +// arity: -1, +// flags: new Set([CommandFlags.STALE, CommandFlags.FAST]), +// firstKeyIndex: 0, +// lastKeyIndex: 0, +// step: 0, +// categories: new Set([CommandCategories.FAST, CommandCategories.CONNECTION]) +// } +// ); +// }); + +// describe('pushSlotRangesArguments', () => { +// it('single range', () => { +// assert.deepEqual( +// pushSlotRangesArguments([], { +// start: 0, +// end: 1 +// }), +// ['0', '1'] +// ); +// }); + +// it('multiple ranges', () => { +// assert.deepEqual( +// pushSlotRangesArguments([], [{ +// start: 0, +// end: 1 +// }, { +// start: 2, +// end: 3 +// }]), +// ['0', '1', '2', '3'] +// ); +// }); +// }); +// }); diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index 4cf610a036e..cc7100d90e6 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -1,697 +1,641 @@ -import { RedisCommandArgument, RedisCommandArguments } from '.'; +import { RESP_TYPES } from '../RESP/decoder'; +import { UnwrapReply, ArrayReply, BlobStringReply, BooleanReply, CommandArguments, DoubleReply, NullReply, NumberReply, RedisArgument, TuplesReply, MapReply, TypeMapping } from '../RESP/types'; -export function transformBooleanReply(reply: number): boolean { - return reply === 1; +export function isNullReply(reply: unknown): reply is NullReply { + return reply === null; } -export function transformBooleanArrayReply(reply: Array): Array { - return reply.map(transformBooleanReply); +export function isArrayReply(reply: unknown): reply is ArrayReply { + return Array.isArray(reply); } -export type BitValue = 0 | 1; +export const transformBooleanReply = { + 2: (reply: NumberReply<0 | 1>) => reply as unknown as UnwrapReply === 1, + 3: undefined as unknown as () => BooleanReply +}; -export interface ScanOptions { - MATCH?: string; - COUNT?: number; -} +export const transformBooleanArrayReply = { + 2: (reply: ArrayReply>) => { + return (reply as unknown as UnwrapReply).map(transformBooleanReply[2]); + }, + 3: undefined as unknown as () => ArrayReply +}; -export function pushScanArguments( - args: RedisCommandArguments, - cursor: number, - options?: ScanOptions -): RedisCommandArguments { - args.push(cursor.toString()); +export type BitValue = 0 | 1; - if (options?.MATCH) { - args.push('MATCH', options.MATCH); - } +export function transformDoubleArgument(num: number): string { + switch (num) { + case Infinity: + return '+inf'; + + case -Infinity: + return '-inf'; + + default: + return num.toString(); + } +} + +export function transformStringDoubleArgument(num: RedisArgument | number): RedisArgument { + if (typeof num !== 'number') return num; + + return transformDoubleArgument(num); +} + +export const transformDoubleReply = { + 2: (reply: BlobStringReply, preserve?: any, typeMapping?: TypeMapping): DoubleReply => { + const double = typeMapping ? typeMapping[RESP_TYPES.DOUBLE] : undefined; + + switch (double) { + case String: { + return reply as unknown as DoubleReply; + } + default: { + let ret: number; + + switch (reply.toString()) { + case 'inf': + case '+inf': + ret = Infinity; + + case '-inf': + ret = -Infinity; + + case 'nan': + ret = NaN; + + default: + ret = Number(reply); + } - if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + return ret as unknown as DoubleReply; + } } + }, + 3: undefined as unknown as () => DoubleReply +}; - return args; +export function createTransformDoubleReplyResp2Func(preserve?: any, typeMapping?: TypeMapping) { + return (reply: BlobStringReply) => { + return transformDoubleReply[2](reply, preserve, typeMapping); + } } -export function transformNumberInfinityReply(reply: RedisCommandArgument): number { - switch (reply.toString()) { - case '+inf': - return Infinity; - - case '-inf': - return -Infinity; +export const transformDoubleArrayReply = { + 2: (reply: Array, preserve?: any, typeMapping?: TypeMapping) => { + return reply.map(createTransformDoubleReplyResp2Func(preserve, typeMapping)); + }, + 3: undefined as unknown as () => ArrayReply +} - default: - return Number(reply); - } +export function createTransformNullableDoubleReplyResp2Func(preserve?: any, typeMapping?: TypeMapping) { + return (reply: BlobStringReply | NullReply) => { + return transformNullableDoubleReply[2](reply, preserve, typeMapping); + } } -export function transformNumberInfinityNullReply(reply: RedisCommandArgument | null): number | null { +export const transformNullableDoubleReply = { + 2: (reply: BlobStringReply | NullReply, preserve?: any, typeMapping?: TypeMapping) => { if (reply === null) return null; + + return transformDoubleReply[2](reply as BlobStringReply, preserve, typeMapping); + }, + 3: undefined as unknown as () => DoubleReply | NullReply +}; - return transformNumberInfinityReply(reply); +export interface Stringable { + toString(): string; } -export function transformNumberInfinityNullArrayReply(reply: Array): Array { - return reply.map(transformNumberInfinityNullReply); +export function createTransformTuplesReplyFunc(preserve?: any, typeMapping?: TypeMapping) { + return (reply: ArrayReply) => { + return transformTuplesReply(reply, preserve, typeMapping); + }; } -export function transformNumberInfinityArgument(num: number): string { - switch (num) { - case Infinity: - return '+inf'; +export function transformTuplesReply( + reply: ArrayReply, + preserve?: any, + typeMapping?: TypeMapping +): MapReply { + const mapType = typeMapping ? typeMapping[RESP_TYPES.MAP] : undefined; - case -Infinity: - return '-inf'; + const inferred = reply as unknown as UnwrapReply - default: - return num.toString(); + switch (mapType) { + case Array: { + return reply as unknown as MapReply; } -} - -export function transformStringNumberInfinityArgument(num: RedisCommandArgument | number): RedisCommandArgument { - if (typeof num !== 'number') return num; - - return transformNumberInfinityArgument(num); -} + case Map: { + const ret = new Map; -export function transformTuplesReply( - reply: Array -): Record { - const message = Object.create(null); + for (let i = 0; i < inferred.length; i += 2) { + ret.set(inferred[i].toString(), inferred[i + 1] as any); + } - for (let i = 0; i < reply.length; i += 2) { - message[reply[i].toString()] = reply[i + 1]; + return ret as unknown as MapReply;; } + default: { + const ret: Record = Object.create(null); - return message; -} - -export interface StreamMessageReply { - id: RedisCommandArgument; - message: Record; -} - -export function transformStreamMessageReply([id, message]: Array): StreamMessageReply { - return { - id, - message: transformTuplesReply(message) - }; -} - -export function transformStreamMessageNullReply(reply: Array): StreamMessageReply | null { - if (reply === null) return null; - return transformStreamMessageReply(reply); -} - + for (let i = 0; i < inferred.length; i += 2) { + ret[inferred[i].toString()] = inferred[i + 1] as any; + } -export type StreamMessagesReply = Array; -export function transformStreamMessagesReply(reply: Array): StreamMessagesReply { - return reply.map(transformStreamMessageReply); + return ret as unknown as MapReply;; + } + } } -export type StreamMessagesNullReply = Array; -export function transformStreamMessagesNullReply(reply: Array): StreamMessagesNullReply { - return reply.map(transformStreamMessageNullReply); +export interface SortedSetMember { + value: RedisArgument; + score: number; } -export type StreamsMessagesReply = Array<{ - name: RedisCommandArgument; - messages: StreamMessagesReply; -}> | null; - -export function transformStreamsMessagesReply(reply: Array | null): StreamsMessagesReply | null { - if (reply === null) return null; +export type SortedSetSide = 'MIN' | 'MAX'; - return reply.map(([name, rawMessages]) => ({ - name, - messages: transformStreamMessagesReply(rawMessages) - })); -} +export const transformSortedSetReply = { + 2: (reply: ArrayReply, preserve?: any, typeMapping?: TypeMapping) => { + const inferred = reply as unknown as UnwrapReply, + members = []; + for (let i = 0; i < inferred.length; i += 2) { + members.push({ + value: inferred[i], + score: transformDoubleReply[2](inferred[i + 1], preserve, typeMapping) + }); + } -export interface ZMember { - score: number; - value: RedisCommandArgument; + return members; + }, + 3: (reply: ArrayReply>) => { + return (reply as unknown as UnwrapReply).map(member => { + const [value, score] = member as unknown as UnwrapReply; + return { + value, + score + }; + }); + } } -export function transformSortedSetMemberNullReply( - reply: [RedisCommandArgument, RedisCommandArgument] | [] -): ZMember | null { - if (!reply.length) return null; +export type ListSide = 'LEFT' | 'RIGHT'; - return transformSortedSetMemberReply(reply); +export function transformEXAT(EXAT: number | Date): string { + return (typeof EXAT === 'number' ? EXAT : Math.floor(EXAT.getTime() / 1000)).toString(); } -export function transformSortedSetMemberReply( - reply: [RedisCommandArgument, RedisCommandArgument] -): ZMember { - return { - value: reply[0], - score: transformNumberInfinityReply(reply[1]) - }; +export function transformPXAT(PXAT: number | Date): string { + return (typeof PXAT === 'number' ? PXAT : PXAT.getTime()).toString(); } -export function transformSortedSetWithScoresReply(reply: Array): Array { - const members = []; - - for (let i = 0; i < reply.length; i += 2) { - members.push({ - value: reply[i], - score: transformNumberInfinityReply(reply[i + 1]) - }); - } - - return members; +export interface EvalOptions { + keys?: Array; + arguments?: Array; } -export type SortedSetSide = 'MIN' | 'MAX'; - -export interface ZMPopOptions { - COUNT?: number; +export function evalFirstKeyIndex(options?: EvalOptions): string | undefined { + return options?.keys?.[0]; } -export function transformZMPopArguments( - args: RedisCommandArguments, - keys: RedisCommandArgument | Array, - side: SortedSetSide, - options?: ZMPopOptions -): RedisCommandArguments { - pushVerdictArgument(args, keys); - - args.push(side); +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?.COUNT) { - args.push('COUNT', options.COUNT.toString()); - } + if (options?.arguments) { + args.push(...options.arguments); + } - return args; + return args; } -export type ListSide = 'LEFT' | 'RIGHT'; +export function pushVariadicArguments(args: CommandArguments, value: RedisVariadicArgument): CommandArguments { + if (Array.isArray(value)) { + // https://github.com/redis/node-redis/pull/2160 + args = args.concat(value); + } else { + args.push(value); + } -export interface LMPopOptions { - COUNT?: number; + return args; } -export function transformLMPopArguments( - args: RedisCommandArguments, - keys: RedisCommandArgument | Array, - side: ListSide, - options?: LMPopOptions -): RedisCommandArguments { - pushVerdictArgument(args, keys); - - args.push(side); - - if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); +export function pushVariadicNumberArguments( + args: CommandArguments, + value: number | Array +): CommandArguments { + if (Array.isArray(value)) { + for (const item of value) { + args.push(item.toString()); } + } else { + args.push(value.toString()); + } - return args; + return args; } -type GeoCountArgument = number | { - value: number; - ANY?: true -}; - -export function pushGeoCountArgument( - args: RedisCommandArguments, - count: GeoCountArgument | undefined -): RedisCommandArguments { - 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 RedisVariadicArgument = RedisArgument | Array; -export type GeoUnits = 'm' | 'km' | 'mi' | 'ft'; +export function pushVariadicArgument( + args: Array, + value: RedisVariadicArgument +): CommandArguments { + if (Array.isArray(value)) { + args.push(value.length.toString(), ...value); + } else { + args.push('1', value); + } -export interface GeoCoordinates { - longitude: string | number; - latitude: string | number; + return args; } -type GeoSearchFromMember = string; - -export type GeoSearchFrom = GeoSearchFromMember | GeoCoordinates; +export function pushOptionalVariadicArgument( + args: CommandArguments, + name: RedisArgument, + value?: RedisVariadicArgument +): CommandArguments { + if (value === undefined) return args; -interface GeoSearchByRadius { - radius: number; - unit: GeoUnits; -} + args.push(name); -interface GeoSearchByBox { - width: number; - height: number; - unit: GeoUnits; + return pushVariadicArgument(args, value); } -export type GeoSearchBy = GeoSearchByRadius | GeoSearchByBox; - -export interface GeoSearchOptions { - SORT?: 'ASC' | 'DESC'; - COUNT?: GeoCountArgument; +export enum CommandFlags { + WRITE = 'write', // command may result in modifications + READONLY = 'readonly', // command will never modify keys + DENYOOM = 'denyoom', // reject command if currently out of memory + ADMIN = 'admin', // server admin command + PUBSUB = 'pubsub', // pubsub-related command + NOSCRIPT = 'noscript', // deny this command from scripts + RANDOM = 'random', // command has random results, dangerous for scripts + SORT_FOR_SCRIPT = 'sort_for_script', // if called from script, sort output + LOADING = 'loading', // allow command while database is loading + STALE = 'stale', // allow command while replica has stale data + SKIP_MONITOR = 'skip_monitor', // do not show this command in MONITOR + ASKING = 'asking', // cluster related - accept even if importing + FAST = 'fast', // command operates in constant or log(N) time. Used for latency monitoring. + MOVABLEKEYS = 'movablekeys' // keys have no pre-determined position. You must discover keys yourself. } -export function pushGeoSearchArguments( - args: RedisCommandArguments, - key: RedisCommandArgument, - from: GeoSearchFrom, - by: GeoSearchBy, - options?: GeoSearchOptions -): RedisCommandArguments { - 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()); - } - - args.push(by.unit); - - if (options?.SORT) { - args.push(options.SORT); - } - - pushGeoCountArgument(args, options?.COUNT); - - return args; +export enum CommandCategories { + KEYSPACE = '@keyspace', + READ = '@read', + WRITE = '@write', + SET = '@set', + SORTEDSET = '@sortedset', + LIST = '@list', + HASH = '@hash', + STRING = '@string', + BITMAP = '@bitmap', + HYPERLOGLOG = '@hyperloglog', + GEO = '@geo', + STREAM = '@stream', + PUBSUB = '@pubsub', + ADMIN = '@admin', + FAST = '@fast', + SLOW = '@slow', + BLOCKING = '@blocking', + DANGEROUS = '@dangerous', + CONNECTION = '@connection', + TRANSACTION = '@transaction', + SCRIPTING = '@scripting' } -export function pushGeoRadiusArguments( - args: RedisCommandArguments, - key: RedisCommandArgument, - from: GeoSearchFrom, - radius: number, - unit: GeoUnits, - options?: GeoSearchOptions -): RedisCommandArguments { - args.push(key); - - if (typeof from === 'string') { - args.push(from); - } else { - args.push( - from.longitude.toString(), - from.latitude.toString() - ); - } - - args.push( - radius.toString(), - unit - ); - - if (options?.SORT) { - args.push(options.SORT); - } +export type CommandRawReply = [ + name: string, + arity: number, + flags: Array, + firstKeyIndex: number, + lastKeyIndex: number, + step: number, + categories: Array +]; - pushGeoCountArgument(args, options?.COUNT); +export type CommandReply = { + name: string, + arity: number, + flags: Set, + firstKeyIndex: number, + lastKeyIndex: number, + step: number, + categories: Set +}; - return args; +export function transformCommandReply( + this: void, + [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories]: CommandRawReply +): CommandReply { + return { + name, + arity, + flags: new Set(flags), + firstKeyIndex, + lastKeyIndex, + step, + categories: new Set(categories) + }; } -export interface GeoRadiusStoreOptions extends GeoSearchOptions { - STOREDIST?: boolean; +export enum RedisFunctionFlags { + NO_WRITES = 'no-writes', + ALLOW_OOM = 'allow-oom', + ALLOW_STALE = 'allow-stale', + NO_CLUSTER = 'no-cluster' } -export function pushGeoRadiusStoreArguments( - args: RedisCommandArguments, - key: RedisCommandArgument, - from: GeoSearchFrom, - radius: number, - unit: GeoUnits, - destination: RedisCommandArgument, - options?: GeoRadiusStoreOptions -): RedisCommandArguments { - pushGeoRadiusArguments(args, key, from, radius, unit, options); - - if (options?.STOREDIST) { - args.push('STOREDIST', destination); - } else { - args.push('STORE', destination); - } +export type FunctionListRawItemReply = [ + 'library_name', + string, + 'engine', + string, + 'functions', + Array<[ + 'name', + string, + 'description', + string | null, + 'flags', + Array + ]> +]; - return args; +export interface FunctionListItemReply { + libraryName: string; + engine: string; + functions: Array<{ + name: string; + description: string | null; + flags: Array; + }>; } -export enum GeoReplyWith { - DISTANCE = 'WITHDIST', - HASH = 'WITHHASH', - COORDINATES = 'WITHCOORD' +export function transformFunctionListItemReply(reply: FunctionListRawItemReply): FunctionListItemReply { + return { + libraryName: reply[1], + engine: reply[3], + functions: reply[5].map(fn => ({ + name: fn[1], + description: fn[3], + flags: fn[5] + })) + }; } -export interface GeoReplyWithMember { - member: string; - distance?: number; - hash?: string; - coordinates?: { - longitude: string; - latitude: string; - }; +export interface SlotRange { + start: number; + end: number; } -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; - }); +function pushSlotRangeArguments( + args: CommandArguments, + range: SlotRange +): void { + args.push( + range.start.toString(), + range.end.toString() + ); } -export function transformEXAT(EXAT: number | Date): string { - return (typeof EXAT === 'number' ? EXAT : Math.floor(EXAT.getTime() / 1000)).toString(); -} +export function pushSlotRangesArguments( + args: CommandArguments, + ranges: SlotRange | Array +): CommandArguments { + if (Array.isArray(ranges)) { + for (const range of ranges) { + pushSlotRangeArguments(args, range); + } + } else { + pushSlotRangeArguments(args, ranges); + } -export function transformPXAT(PXAT: number | Date): string { - return (typeof PXAT === 'number' ? PXAT : PXAT.getTime()).toString(); + return args; } -export interface EvalOptions { - keys?: Array; - arguments?: Array; -} +export type RawRangeReply = [ + start: number, + end: number +]; -export function evalFirstKeyIndex(options?: EvalOptions): string | undefined { - return options?.keys?.[0]; +export interface RangeReply { + start: number; + end: number; } -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 function transformRangeReply([start, end]: RawRangeReply): RangeReply { + return { + start, + end + }; } -export function pushVerdictArguments(args: RedisCommandArguments, value: RedisCommandArgument | Array): RedisCommandArguments { - if (Array.isArray(value)) { - // https://github.com/redis/node-redis/pull/2160 - args = args.concat(value); - } else { - args.push(value); - } - - return args; -} +export type ZKeyAndWeight = { + key: RedisArgument; + weight: number; +}; -export function pushVerdictNumberArguments( - args: RedisCommandArguments, - value: number | Array -): RedisCommandArguments { - if (Array.isArray(value)) { - for (const item of value) { - args.push(item.toString()); +export type ZVariadicKeys = T | [T, ...Array]; + +export type ZKeys = ZVariadicKeys | ZVariadicKeys; + +export function pushZKeysArguments( + args: CommandArguments, + keys: ZKeys +) { + if (Array.isArray(keys)) { + args.push(keys.length.toString()); + + if (keys.length) { + if (isPlainKeys(keys)) { + args = args.concat(keys); + } else { + const start = args.length; + args[start + keys.length] = 'WEIGHTS'; + for (let i = 0; i < keys.length; i++) { + const index = start + i; + args[index] = keys[i].key; + args[index + 1 + keys.length] = transformDoubleArgument(keys[i].weight); } - } else { - args.push(value.toString()); + } } + } else { + args.push('1'); - return args; -} - -export function pushVerdictArgument( - args: RedisCommandArguments, - value: RedisCommandArgument | Array -): RedisCommandArguments { - if (Array.isArray(value)) { - args.push(value.length.toString(), ...value); + if (isPlainKey(keys)) { + args.push(keys); } else { - args.push('1', value); + args.push( + keys.key, + 'WEIGHTS', + transformDoubleArgument(keys.weight) + ); } + } - return args; -} - -export function pushOptionalVerdictArgument( - args: RedisCommandArguments, - name: RedisCommandArgument, - value: undefined | RedisCommandArgument | Array -): RedisCommandArguments { - if (value === undefined) return args; - - args.push(name); - - return pushVerdictArgument(args, value); + return args; } -export enum CommandFlags { - WRITE = 'write', // command may result in modifications - READONLY = 'readonly', // command will never modify keys - DENYOOM = 'denyoom', // reject command if currently out of memory - ADMIN = 'admin', // server admin command - PUBSUB = 'pubsub', // pubsub-related command - NOSCRIPT = 'noscript', // deny this command from scripts - RANDOM = 'random', // command has random results, dangerous for scripts - SORT_FOR_SCRIPT = 'sort_for_script', // if called from script, sort output - LOADING = 'loading', // allow command while database is loading - STALE = 'stale', // allow command while replica has stale data - SKIP_MONITOR = 'skip_monitor', // do not show this command in MONITOR - ASKING = 'asking', // cluster related - accept even if importing - FAST = 'fast', // command operates in constant or log(N) time. Used for latency monitoring. - MOVABLEKEYS = 'movablekeys' // keys have no pre-determined position. You must discover keys yourself. +function isPlainKey(key: RedisArgument | ZKeyAndWeight): key is RedisArgument { + return typeof key === 'string' || key instanceof Buffer; } -export enum CommandCategories { - KEYSPACE = '@keyspace', - READ = '@read', - WRITE = '@write', - SET = '@set', - SORTEDSET = '@sortedset', - LIST = '@list', - HASH = '@hash', - STRING = '@string', - BITMAP = '@bitmap', - HYPERLOGLOG = '@hyperloglog', - GEO = '@geo', - STREAM = '@stream', - PUBSUB = '@pubsub', - ADMIN = '@admin', - FAST = '@fast', - SLOW = '@slow', - BLOCKING = '@blocking', - DANGEROUS = '@dangerous', - CONNECTION = '@connection', - TRANSACTION = '@transaction', - SCRIPTING = '@scripting' +function isPlainKeys(keys: Array | Array): keys is Array { + return isPlainKey(keys[0]); } -export type CommandRawReply = [ - name: string, - arity: number, - flags: Array, - firstKeyIndex: number, - lastKeyIndex: number, - step: number, - categories: Array -]; +export type StreamMessageRawReply = TuplesReply<[ + id: BlobStringReply, + message: ArrayReply +]>; -export type CommandReply = { - name: string, - arity: number, - flags: Set, - firstKeyIndex: number, - lastKeyIndex: number, - step: number, - categories: Set +export type StreamMessageReply = { + id: BlobStringReply, + message: MapReply, }; -export function transformCommandReply( - this: void, - [name, arity, flags, firstKeyIndex, lastKeyIndex, step, categories]: CommandRawReply -): CommandReply { - return { - name, - arity, - flags: new Set(flags), - firstKeyIndex, - lastKeyIndex, - step, - categories: new Set(categories) - }; +export function transformStreamMessageReply(typeMapping: TypeMapping | undefined, reply: StreamMessageRawReply): StreamMessageReply { + const [ id, message ] = reply as unknown as UnwrapReply; + return { + id: id, + message: transformTuplesReply(message, undefined, typeMapping) + }; } -export enum RedisFunctionFlags { - NO_WRITES = 'no-writes', - ALLOW_OOM = 'allow-oom', - ALLOW_STALE = 'allow-stale', - NO_CLUSTER = 'no-cluster' +export function transformStreamMessageNullReply(typeMapping: TypeMapping | undefined, reply: StreamMessageRawReply | NullReply) { + return isNullReply(reply) ? reply : transformStreamMessageReply(typeMapping, reply); } -export type FunctionListRawItemReply = [ - 'library_name', - string, - 'engine', - string, - 'functions', - Array<[ - 'name', - string, - 'description', - string | null, - 'flags', - Array - ]> -]; +export type StreamMessagesReply = Array; -export interface FunctionListItemReply { - libraryName: string; - engine: string; - functions: Array<{ - name: string; - description: string | null; - flags: Array; - }>; -} +export type StreamsMessagesReply = Array<{ + name: BlobStringReply | string; + messages: StreamMessagesReply; +}> | null; -export function transformFunctionListItemReply(reply: FunctionListRawItemReply): FunctionListItemReply { - return { - libraryName: reply[1], - engine: reply[3], - functions: reply[5].map(fn => ({ - name: fn[1], - description: fn[3], - flags: fn[5] - })) - }; -} - -export interface SortOptions { - BY?: string; - LIMIT?: { - offset: number; - count: number; - }, - GET?: string | Array; - DIRECTION?: 'ASC' | 'DESC'; - ALPHA?: true; -} - -export function pushSortArguments( - args: RedisCommandArguments, - options?: SortOptions -): RedisCommandArguments { - if (options?.BY) { - args.push('BY', options.BY); +export function transformStreamMessagesReply( + r: ArrayReply, + typeMapping?: TypeMapping +): StreamMessagesReply { + const reply = r as unknown as UnwrapReply; + + return reply.map(transformStreamMessageReply.bind(undefined, typeMapping)); +} + +type StreamMessagesRawReply = TuplesReply<[name: BlobStringReply, ArrayReply]>; +type StreamsMessagesRawReply2 = ArrayReply; + +export function transformStreamsMessagesReplyResp2( + reply: UnwrapReply, + preserve?: any, + typeMapping?: TypeMapping +): StreamsMessagesReply | NullReply { + // FUTURE: resposne type if resp3 was working, reverting to old v4 for now + //: MapReply | NullReply { + if (reply === null) return null as unknown as NullReply; + + switch (typeMapping? typeMapping[RESP_TYPES.MAP] : undefined) { +/* FUTURE: a response type for when resp3 is working properly + case Map: { + const ret = new Map(); + + for (let i=0; i < reply.length; i++) { + const stream = reply[i] as unknown as UnwrapReply; + + const name = stream[0]; + const rawMessages = stream[1]; + + ret.set(name.toString(), transformStreamMessagesReply(rawMessages, typeMapping)); + } + + return ret as unknown as MapReply; } - - if (options?.LIMIT) { - args.push( - 'LIMIT', - options.LIMIT.offset.toString(), - options.LIMIT.count.toString() - ); + case Array: { + const ret: Array = []; + + for (let i=0; i < reply.length; i++) { + const stream = reply[i] as unknown as UnwrapReply; + + const name = stream[0]; + const rawMessages = stream[1]; + + ret.push(name); + ret.push(transformStreamMessagesReply(rawMessages, typeMapping)); + } + + return ret as unknown as MapReply; } - - if (options?.GET) { - for (const pattern of (typeof options.GET === 'string' ? [options.GET] : options.GET)) { - args.push('GET', pattern); - } + default: { + const ret: Record = Object.create(null); + + for (let i=0; i < reply.length; i++) { + const stream = reply[i] as unknown as UnwrapReply; + + const name = stream[0] as unknown as UnwrapReply; + const rawMessages = stream[1]; + + ret[name.toString()] = transformStreamMessagesReply(rawMessages); + } + + return ret as unknown as MapReply; } +*/ + // V4 compatible response type + default: { + const ret: StreamsMessagesReply = []; - if (options?.DIRECTION) { - args.push(options.DIRECTION); - } + for (let i=0; i < reply.length; i++) { + const stream = reply[i] as unknown as UnwrapReply; - if (options?.ALPHA) { - args.push('ALPHA'); - } + ret.push({ + name: stream[0], + messages: transformStreamMessagesReply(stream[1]) + }); + } - return args; + return ret; + } + } } -export interface SlotRange { - start: number; - end: number; -} +type StreamsMessagesRawReply3 = MapReply>; -function pushSlotRangeArguments( - args: RedisCommandArguments, - range: SlotRange -): void { - args.push( - range.start.toString(), - range.end.toString() - ); -} +export function transformStreamsMessagesReplyResp3(reply: UnwrapReply): MapReply | NullReply { + if (reply === null) return null as unknown as NullReply; + + if (reply instanceof Map) { + const ret = new Map(); -export function pushSlotRangesArguments( - args: RedisCommandArguments, - ranges: SlotRange | Array -): RedisCommandArguments { - if (Array.isArray(ranges)) { - for (const range of ranges) { - pushSlotRangeArguments(args, range); - } - } else { - pushSlotRangeArguments(args, ranges); + for (const [n, rawMessages] of reply) { + const name = n as unknown as UnwrapReply; + + ret.set(name.toString(), transformStreamMessagesReply(rawMessages)); } - return args; -} + return ret as unknown as MapReply + } else if (reply instanceof Array) { + const ret = []; -export type RawRangeReply = [ - start: number, - end: number -]; + for (let i=0; i < reply.length; i += 2) { + const name = reply[i] as BlobStringReply; + const rawMessages = reply[i+1] as ArrayReply; -export interface RangeReply { - start: number; - end: number; -} + ret.push(name); + ret.push(transformStreamMessagesReply(rawMessages)); + } -export function transformRangeReply([start, end]: RawRangeReply): RangeReply { - return { - start, - end - }; + return ret as unknown as MapReply + } else { + const ret = Object.create(null); + for (const [name, rawMessages] of Object.entries(reply)) { + ret[name] = transformStreamMessagesReply(rawMessages); + } + + return ret as unknown as MapReply + } } diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 60f9720c8d1..024ee2191b8 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -1,91 +1,1032 @@ -import { ClientCommandOptions } from '../client'; -import { CommandOptions } from '../command-options'; -import { RedisScriptConfig, SHA1 } from '../lua-script'; +import type { RedisCommands } from '../RESP/types'; +import ACL_CAT from './ACL_CAT'; +import ACL_DELUSER from './ACL_DELUSER'; +import ACL_DRYRUN from './ACL_DRYRUN'; +import ACL_GENPASS from './ACL_GENPASS'; +import ACL_GETUSER from './ACL_GETUSER'; +import ACL_LIST from './ACL_LIST'; +import ACL_LOAD from './ACL_LOAD'; +import ACL_LOG_RESET from './ACL_LOG_RESET'; +import ACL_LOG from './ACL_LOG'; +import ACL_SAVE from './ACL_SAVE'; +import ACL_SETUSER from './ACL_SETUSER'; +import ACL_USERS from './ACL_USERS'; +import ACL_WHOAMI from './ACL_WHOAMI'; +import APPEND from './APPEND'; +import ASKING from './ASKING'; +import AUTH from './AUTH'; +import BGREWRITEAOF from './BGREWRITEAOF'; +import BGSAVE from './BGSAVE'; +import BITCOUNT from './BITCOUNT'; +import BITFIELD_RO from './BITFIELD_RO'; +import BITFIELD from './BITFIELD'; +import BITOP from './BITOP'; +import BITPOS from './BITPOS'; +import BLMOVE from './BLMOVE'; +import BLMPOP from './BLMPOP'; +import BLPOP from './BLPOP'; +import BRPOP from './BRPOP'; +import BRPOPLPUSH from './BRPOPLPUSH'; +import BZMPOP from './BZMPOP'; +import BZPOPMAX from './BZPOPMAX'; +import BZPOPMIN from './BZPOPMIN'; +import CLIENT_CACHING from './CLIENT_CACHING'; +import CLIENT_GETNAME from './CLIENT_GETNAME'; +import CLIENT_GETREDIR from './CLIENT_GETREDIR'; +import CLIENT_ID from './CLIENT_ID'; +import CLIENT_INFO from './CLIENT_INFO'; +import CLIENT_KILL from './CLIENT_KILL'; +import CLIENT_LIST from './CLIENT_LIST'; +import CLIENT_NO_EVICT from './CLIENT_NO-EVICT'; +import CLIENT_NO_TOUCH from './CLIENT_NO-TOUCH'; +import CLIENT_PAUSE from './CLIENT_PAUSE'; +import CLIENT_SETNAME from './CLIENT_SETNAME'; +import CLIENT_TRACKING from './CLIENT_TRACKING'; +import CLIENT_TRACKINGINFO from './CLIENT_TRACKINGINFO'; +import CLIENT_UNPAUSE from './CLIENT_UNPAUSE'; +import CLUSTER_ADDSLOTS from './CLUSTER_ADDSLOTS'; +import CLUSTER_ADDSLOTSRANGE from './CLUSTER_ADDSLOTSRANGE'; +import CLUSTER_BUMPEPOCH from './CLUSTER_BUMPEPOCH'; +import CLUSTER_COUNT_FAILURE_REPORTS from './CLUSTER_COUNT-FAILURE-REPORTS'; +import CLUSTER_COUNTKEYSINSLOT from './CLUSTER_COUNTKEYSINSLOT'; +import CLUSTER_DELSLOTS from './CLUSTER_DELSLOTS'; +import CLUSTER_DELSLOTSRANGE from './CLUSTER_DELSLOTSRANGE'; +import CLUSTER_FAILOVER from './CLUSTER_FAILOVER'; +import CLUSTER_FLUSHSLOTS from './CLUSTER_FLUSHSLOTS'; +import CLUSTER_FORGET from './CLUSTER_FORGET'; +import CLUSTER_GETKEYSINSLOT from './CLUSTER_GETKEYSINSLOT'; +import CLUSTER_INFO from './CLUSTER_INFO'; +import CLUSTER_KEYSLOT from './CLUSTER_KEYSLOT'; +import CLUSTER_LINKS from './CLUSTER_LINKS'; +import CLUSTER_MEET from './CLUSTER_MEET'; +import CLUSTER_MYID from './CLUSTER_MYID'; +import CLUSTER_MYSHARDID from './CLUSTER_MYSHARDID'; +import CLUSTER_NODES from './CLUSTER_NODES'; +import CLUSTER_REPLICAS from './CLUSTER_REPLICAS'; +import CLUSTER_REPLICATE from './CLUSTER_REPLICATE'; +import CLUSTER_RESET from './CLUSTER_RESET'; +import CLUSTER_SAVECONFIG from './CLUSTER_SAVECONFIG'; +import CLUSTER_SET_CONFIG_EPOCH from './CLUSTER_SET-CONFIG-EPOCH'; +import CLUSTER_SETSLOT from './CLUSTER_SETSLOT'; +import CLUSTER_SLOTS from './CLUSTER_SLOTS'; +import COMMAND_COUNT from './COMMAND_COUNT'; +import COMMAND_GETKEYS from './COMMAND_GETKEYS'; +import COMMAND_GETKEYSANDFLAGS from './COMMAND_GETKEYSANDFLAGS'; +import COMMAND_INFO from './COMMAND_INFO'; +import COMMAND_LIST from './COMMAND_LIST'; +import COMMAND from './COMMAND'; +import CONFIG_GET from './CONFIG_GET'; +import CONFIG_RESETASTAT from './CONFIG_RESETSTAT'; +import CONFIG_REWRITE from './CONFIG_REWRITE'; +import CONFIG_SET from './CONFIG_SET'; +import COPY from './COPY'; +import DBSIZE from './DBSIZE'; +import DECR from './DECR'; +import DECRBY from './DECRBY'; +import DEL from './DEL'; +import DUMP from './DUMP'; +import ECHO from './ECHO'; +import EVAL_RO from './EVAL_RO'; +import EVAL from './EVAL'; +import EVALSHA_RO from './EVALSHA_RO'; +import EVALSHA from './EVALSHA'; +import GEOADD from './GEOADD'; +import GEODIST from './GEODIST'; +import GEOHASH from './GEOHASH'; +import GEOPOS from './GEOPOS'; +import GEORADIUS_RO_WITH from './GEORADIUS_RO_WITH'; +import GEORADIUS_RO from './GEORADIUS_RO'; +import GEORADIUS_STORE from './GEORADIUS_STORE'; +import GEORADIUS_WITH from './GEORADIUS_WITH'; +import GEORADIUS from './GEORADIUS'; +import GEORADIUSBYMEMBER_RO_WITH from './GEORADIUSBYMEMBER_RO_WITH'; +import GEORADIUSBYMEMBER_RO from './GEORADIUSBYMEMBER_RO'; +import GEORADIUSBYMEMBER_STORE from './GEORADIUSBYMEMBER_STORE'; +import GEORADIUSBYMEMBER_WITH from './GEORADIUSBYMEMBER_WITH'; +import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER'; +import GEOSEARCH_WITH from './GEOSEARCH_WITH'; +import GEOSEARCH from './GEOSEARCH'; +import GEOSEARCHSTORE from './GEOSEARCHSTORE'; +import GET from './GET'; +import GETBIT from './GETBIT'; +import GETDEL from './GETDEL'; +import GETEX from './GETEX'; +import GETRANGE from './GETRANGE'; +import GETSET from './GETSET'; +import EXISTS from './EXISTS'; +import EXPIRE from './EXPIRE'; +import EXPIREAT from './EXPIREAT'; +import EXPIRETIME from './EXPIRETIME'; +import FLUSHALL from './FLUSHALL'; +import FLUSHDB from './FLUSHDB'; +import FCALL from './FCALL'; +import FCALL_RO from './FCALL_RO'; +import FUNCTION_DELETE from './FUNCTION_DELETE'; +import FUNCTION_DUMP from './FUNCTION_DUMP'; +import FUNCTION_FLUSH from './FUNCTION_FLUSH'; +import FUNCTION_KILL from './FUNCTION_KILL'; +import FUNCTION_LIST_WITHCODE from './FUNCTION_LIST_WITHCODE'; +import FUNCTION_LIST from './FUNCTION_LIST'; +import FUNCTION_LOAD from './FUNCTION_LOAD'; +import FUNCTION_RESTORE from './FUNCTION_RESTORE'; +import FUNCTION_STATS from './FUNCTION_STATS'; +import HDEL from './HDEL'; +import HELLO from './HELLO'; +import HEXISTS from './HEXISTS'; +import HEXPIRE from './HEXPIRE'; +import HEXPIREAT from './HEXPIREAT'; +import HEXPIRETIME from './HEXPIRETIME'; +import HGET from './HGET'; +import HGETALL from './HGETALL'; +import HINCRBY from './HINCRBY'; +import HINCRBYFLOAT from './HINCRBYFLOAT'; +import HKEYS from './HKEYS'; +import HLEN from './HLEN'; +import HMGET from './HMGET'; +import HPERSIST from './HPERSIST'; +import HPEXPIRE from './HPEXPIRE'; +import HPEXPIREAT from './HPEXPIREAT'; +import HPEXPIRETIME from './HPEXPIRETIME'; +import HPTTL from './HPTTL'; +import HRANDFIELD_COUNT_WITHVALUES from './HRANDFIELD_COUNT_WITHVALUES'; +import HRANDFIELD_COUNT from './HRANDFIELD_COUNT'; +import HRANDFIELD from './HRANDFIELD'; +import HSCAN from './HSCAN'; +import HSCAN_NOVALUES from './HSCAN_NOVALUES'; +import HSET from './HSET'; +import HSETNX from './HSETNX'; +import HSTRLEN from './HSTRLEN'; +import HTTL from './HTTL'; +import HVALS from './HVALS'; +import INCR from './INCR'; +import INCRBY from './INCRBY'; +import INCRBYFLOAT from './INCRBYFLOAT'; +import INFO from './INFO'; +import KEYS from './KEYS'; +import LASTSAVE from './LASTSAVE'; +import LATENCY_DOCTOR from './LATENCY_DOCTOR'; +import LATENCY_GRAPH from './LATENCY_GRAPH'; +import LATENCY_HISTORY from './LATENCY_HISTORY'; +import LATENCY_LATEST from './LATENCY_LATEST'; +import LCS_IDX_WITHMATCHLEN from './LCS_IDX_WITHMATCHLEN'; +import LCS_IDX from './LCS_IDX'; +import LCS_LEN from './LCS_LEN'; +import LCS from './LCS'; +import LINDEX from './LINDEX'; +import LINSERT from './LINSERT'; +import LLEN from './LLEN'; +import LMOVE from './LMOVE'; +import LMPOP from './LMPOP'; +import LOLWUT from './LOLWUT'; +import LPOP_COUNT from './LPOP_COUNT'; +import LPOP from './LPOP'; +import LPOS_COUNT from './LPOS_COUNT'; +import LPOS from './LPOS'; +import LPUSH from './LPUSH'; +import LPUSHX from './LPUSHX'; +import LRANGE from './LRANGE'; +import LREM from './LREM'; +import LSET from './LSET'; +import LTRIM from './LTRIM'; +import MEMORY_DOCTOR from './MEMORY_DOCTOR'; +import MEMORY_MALLOC_STATS from './MEMORY_MALLOC-STATS'; +import MEMORY_PURGE from './MEMORY_PURGE'; +import MEMORY_STATS from './MEMORY_STATS'; +import MEMORY_USAGE from './MEMORY_USAGE'; +import MGET from './MGET'; +import MIGRATE from './MIGRATE'; +import MODULE_LIST from './MODULE_LIST'; +import MODULE_LOAD from './MODULE_LOAD'; +import MODULE_UNLOAD from './MODULE_UNLOAD'; +import MOVE from './MOVE'; +import MSET from './MSET'; +import MSETNX from './MSETNX'; +import OBJECT_ENCODING from './OBJECT_ENCODING'; +import OBJECT_FREQ from './OBJECT_FREQ'; +import OBJECT_IDLETIME from './OBJECT_IDLETIME'; +import OBJECT_REFCOUNT from './OBJECT_REFCOUNT'; +import PERSIST from './PERSIST'; +import PEXPIRE from './PEXPIRE'; +import PEXPIREAT from './PEXPIREAT'; +import PEXPIRETIME from './PEXPIRETIME'; +import PFADD from './PFADD'; +import PFCOUNT from './PFCOUNT'; +import PFMERGE from './PFMERGE'; +import PING from './PING'; +import PSETEX from './PSETEX'; +import PTTL from './PTTL'; +import PUBLISH from './PUBLISH'; +import PUBSUB_CHANNELS from './PUBSUB_CHANNELS'; +import PUBSUB_NUMPAT from './PUBSUB_NUMPAT'; +import PUBSUB_NUMSUB from './PUBSUB_NUMSUB'; +import PUBSUB_SHARDNUMSUB from './PUBSUB_SHARDNUMSUB'; +import PUBSUB_SHARDCHANNELS from './PUBSUB_SHARDCHANNELS'; +import RANDOMKEY from './RANDOMKEY'; +import READONLY from './READONLY'; +import RENAME from './RENAME'; +import RENAMENX from './RENAMENX'; +import REPLICAOF from './REPLICAOF'; +import RESTORE_ASKING from './RESTORE-ASKING'; +import RESTORE from './RESTORE'; +import ROLE from './ROLE'; +import RPOP_COUNT from './RPOP_COUNT'; +import RPOP from './RPOP'; +import RPOPLPUSH from './RPOPLPUSH'; +import RPUSH from './RPUSH'; +import RPUSHX from './RPUSHX'; +import SADD from './SADD'; +import SCAN from './SCAN'; +import SCARD from './SCARD'; +import SCRIPT_DEBUG from './SCRIPT_DEBUG'; +import SCRIPT_EXISTS from './SCRIPT_EXISTS'; +import SCRIPT_FLUSH from './SCRIPT_FLUSH'; +import SCRIPT_KILL from './SCRIPT_KILL'; +import SCRIPT_LOAD from './SCRIPT_LOAD'; +import SDIFF from './SDIFF'; +import SDIFFSTORE from './SDIFFSTORE'; +import SET from './SET'; +import SETBIT from './SETBIT'; +import SETEX from './SETEX'; +import SETNX from './SETNX'; +import SETRANGE from './SETRANGE'; +import SINTER from './SINTER'; +import SINTERCARD from './SINTERCARD'; +import SINTERSTORE from './SINTERSTORE'; +import SISMEMBER from './SISMEMBER'; +import SMEMBERS from './SMEMBERS'; +import SMISMEMBER from './SMISMEMBER'; +import SMOVE from './SMOVE'; +import SORT_RO from './SORT_RO'; +import SORT_STORE from './SORT_STORE'; +import SORT from './SORT'; +import SPOP_COUNT from './SPOP_COUNT'; +import SPOP from './SPOP'; +import SPUBLISH from './SPUBLISH'; +import SRANDMEMBER_COUNT from './SRANDMEMBER_COUNT'; +import SRANDMEMBER from './SRANDMEMBER'; +import SREM from './SREM'; +import SSCAN from './SSCAN'; +import STRLEN from './STRLEN'; +import SUNION from './SUNION'; +import SUNIONSTORE from './SUNIONSTORE'; +import SWAPDB from './SWAPDB'; +import TIME from './TIME'; +import TOUCH from './TOUCH'; +import TTL from './TTL'; +import TYPE from './TYPE'; +import UNLINK from './UNLINK'; +import WAIT from './WAIT'; +import XACK from './XACK'; +import XADD_NOMKSTREAM from './XADD_NOMKSTREAM'; +import XADD from './XADD'; +import XAUTOCLAIM_JUSTID from './XAUTOCLAIM_JUSTID'; +import XAUTOCLAIM from './XAUTOCLAIM'; +import XCLAIM_JUSTID from './XCLAIM_JUSTID'; +import XCLAIM from './XCLAIM'; +import XDEL from './XDEL'; +import XGROUP_CREATE from './XGROUP_CREATE'; +import XGROUP_CREATECONSUMER from './XGROUP_CREATECONSUMER'; +import XGROUP_DELCONSUMER from './XGROUP_DELCONSUMER'; +import XGROUP_DESTROY from './XGROUP_DESTROY'; +import XGROUP_SETID from './XGROUP_SETID'; +import XINFO_CONSUMERS from './XINFO_CONSUMERS'; +import XINFO_GROUPS from './XINFO_GROUPS'; +import XINFO_STREAM from './XINFO_STREAM'; +import XLEN from './XLEN'; +import XPENDING_RANGE from './XPENDING_RANGE'; +import XPENDING from './XPENDING'; +import XRANGE from './XRANGE'; +import XREAD from './XREAD'; +import XREADGROUP from './XREADGROUP'; +import XREVRANGE from './XREVRANGE'; +import XSETID from './XSETID'; +import XTRIM from './XTRIM'; +import ZADD_INCR from './ZADD_INCR'; +import ZADD from './ZADD'; +import ZCARD from './ZCARD'; +import ZCOUNT from './ZCOUNT'; +import ZDIFF_WITHSCORES from './ZDIFF_WITHSCORES'; +import ZDIFF from './ZDIFF'; +import ZDIFFSTORE from './ZDIFFSTORE'; +import ZINCRBY from './ZINCRBY'; +import ZINTER_WITHSCORES from './ZINTER_WITHSCORES'; +import ZINTER from './ZINTER'; +import ZINTERCARD from './ZINTERCARD'; +import ZINTERSTORE from './ZINTERSTORE'; +import ZLEXCOUNT from './ZLEXCOUNT'; +import ZMPOP from './ZMPOP'; +import ZMSCORE from './ZMSCORE'; +import ZPOPMAX_COUNT from './ZPOPMAX_COUNT'; +import ZPOPMAX from './ZPOPMAX'; +import ZPOPMIN_COUNT from './ZPOPMIN_COUNT'; +import ZPOPMIN from './ZPOPMIN'; +import ZRANDMEMBER_COUNT_WITHSCORES from './ZRANDMEMBER_COUNT_WITHSCORES'; +import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; +import ZRANDMEMBER from './ZRANDMEMBER'; +import ZRANGE_WITHSCORES from './ZRANGE_WITHSCORES'; +import ZRANGE from './ZRANGE'; +import ZRANGEBYLEX from './ZRANGEBYLEX'; +import ZRANGEBYSCORE_WITHSCORES from './ZRANGEBYSCORE_WITHSCORES'; +import ZRANGEBYSCORE from './ZRANGEBYSCORE'; +import ZRANGESTORE from './ZRANGESTORE'; +import ZREMRANGEBYSCORE from './ZREMRANGEBYSCORE'; +import ZRANK_WITHSCORE from './ZRANK_WITHSCORE'; +import ZRANK from './ZRANK'; +import ZREM from './ZREM'; +import ZREMRANGEBYLEX from './ZREMRANGEBYLEX'; +import ZREMRANGEBYRANK from './ZREMRANGEBYRANK'; +import ZREVRANK from './ZREVRANK'; +import ZSCAN from './ZSCAN'; +import ZSCORE from './ZSCORE'; +import ZUNION_WITHSCORES from './ZUNION_WITHSCORES'; +import ZUNION from './ZUNION'; +import ZUNIONSTORE from './ZUNIONSTORE'; -export type RedisCommandRawReply = string | number | Buffer | null | undefined | Array; - -export type RedisCommandArgument = string | Buffer; - -export type RedisCommandArguments = Array & { preserve?: unknown }; - -export interface RedisCommand { - FIRST_KEY_INDEX?: number | ((...args: Array) => RedisCommandArgument | undefined); - IS_READ_ONLY?: boolean; - TRANSFORM_LEGACY_REPLY?: boolean; - transformArguments(this: void, ...args: Array): RedisCommandArguments; - transformReply?(this: void, reply: any, preserved?: any): any; -} - -export type RedisCommandReply = - C['transformReply'] extends (...args: any) => infer T ? T : RedisCommandRawReply; - -export type ConvertArgumentType = - Type extends RedisCommandArgument ? ( - Type extends (string & ToType) ? Type : ToType - ) : ( - Type extends Set ? Set> : ( - Type extends Map ? Map> : ( - Type extends Array ? Array> : ( - Type extends Date ? Type : ( - Type extends Record ? { - [Property in keyof Type]: ConvertArgumentType - } : Type - ) - ) - ) - ) - ); - -export type RedisCommandSignature< - Command extends RedisCommand, - Params extends Array = Parameters -> = >( - ...args: Params | [options: Options, ...rest: Params] -) => Promise< - ConvertArgumentType< - RedisCommandReply, - Options['returnBuffers'] extends true ? Buffer : string - > ->; - -export interface RedisCommands { - [command: string]: RedisCommand; -} - -export interface RedisModule { - [command: string]: RedisCommand; -} - -export interface RedisModules { - [module: string]: RedisModule; -} - -export interface RedisFunction extends RedisCommand { - NUMBER_OF_KEYS?: number; -} - -export interface RedisFunctionLibrary { - [fn: string]: RedisFunction; -} - -export interface RedisFunctions { - [library: string]: RedisFunctionLibrary; -} - -export type RedisScript = RedisScriptConfig & SHA1; - -export interface RedisScripts { - [script: string]: RedisScript; -} - -export interface RedisExtensions< - M extends RedisModules = RedisModules, - F extends RedisFunctions = RedisFunctions, - S extends RedisScripts = RedisScripts -> { - modules?: M; - functions?: F; - scripts?: S; -} - -export type ExcludeMappedString = string extends S ? never : S; +export default { + ACL_CAT, + aclCat: ACL_CAT, + ACL_DELUSER, + aclDelUser: ACL_DELUSER, + ACL_DRYRUN, + aclDryRun: ACL_DRYRUN, + ACL_GENPASS, + aclGenPass: ACL_GENPASS, + ACL_GETUSER, + aclGetUser: ACL_GETUSER, + ACL_LIST, + aclList: ACL_LIST, + ACL_LOAD, + aclLoad: ACL_LOAD, + ACL_LOG_RESET, + aclLogReset: ACL_LOG_RESET, + ACL_LOG, + aclLog: ACL_LOG, + ACL_SAVE, + aclSave: ACL_SAVE, + ACL_SETUSER, + aclSetUser: ACL_SETUSER, + ACL_USERS, + aclUsers: ACL_USERS, + ACL_WHOAMI, + aclWhoAmI: ACL_WHOAMI, + APPEND, + append: APPEND, + ASKING, + asking: ASKING, + AUTH, + auth: AUTH, + BGREWRITEAOF, + bgRewriteAof: BGREWRITEAOF, + BGSAVE, + bgSave: BGSAVE, + BITCOUNT, + bitCount: BITCOUNT, + BITFIELD_RO, + bitFieldRo: BITFIELD_RO, + BITFIELD, + bitField: BITFIELD, + BITOP, + bitOp: BITOP, + BITPOS, + bitPos: BITPOS, + BLMOVE, + blMove: BLMOVE, + BLMPOP, + blmPop: BLMPOP, + BLPOP, + blPop: BLPOP, + BRPOP, + brPop: BRPOP, + BRPOPLPUSH, + brPopLPush: BRPOPLPUSH, + BZMPOP, + bzmPop: BZMPOP, + BZPOPMAX, + bzPopMax: BZPOPMAX, + BZPOPMIN, + bzPopMin: BZPOPMIN, + CLIENT_CACHING, + clientCaching: CLIENT_CACHING, + CLIENT_GETNAME, + clientGetName: CLIENT_GETNAME, + CLIENT_GETREDIR, + clientGetRedir: CLIENT_GETREDIR, + CLIENT_ID, + clientId: CLIENT_ID, + CLIENT_INFO, + clientInfo: CLIENT_INFO, + CLIENT_KILL, + clientKill: CLIENT_KILL, + CLIENT_LIST, + clientList: CLIENT_LIST, + 'CLIENT_NO-EVICT': CLIENT_NO_EVICT, + clientNoEvict: CLIENT_NO_EVICT, + 'CLIENT_NO-TOUCH': CLIENT_NO_TOUCH, + clientNoTouch: CLIENT_NO_TOUCH, + CLIENT_PAUSE, + clientPause: CLIENT_PAUSE, + CLIENT_SETNAME, + clientSetName: CLIENT_SETNAME, + CLIENT_TRACKING, + clientTracking: CLIENT_TRACKING, + CLIENT_TRACKINGINFO, + clientTrackingInfo: CLIENT_TRACKINGINFO, + CLIENT_UNPAUSE, + clientUnpause: CLIENT_UNPAUSE, + CLUSTER_ADDSLOTS, + clusterAddSlots: CLUSTER_ADDSLOTS, + CLUSTER_ADDSLOTSRANGE, + clusterAddSlotsRange: CLUSTER_ADDSLOTSRANGE, + CLUSTER_BUMPEPOCH, + clusterBumpEpoch: CLUSTER_BUMPEPOCH, + 'CLUSTER_COUNT-FAILURE-REPORTS': CLUSTER_COUNT_FAILURE_REPORTS, + clusterCountFailureReports: CLUSTER_COUNT_FAILURE_REPORTS, + CLUSTER_COUNTKEYSINSLOT, + clusterCountKeysInSlot: CLUSTER_COUNTKEYSINSLOT, + CLUSTER_DELSLOTS, + clusterDelSlots: CLUSTER_DELSLOTS, + CLUSTER_DELSLOTSRANGE, + clusterDelSlotsRange: CLUSTER_DELSLOTSRANGE, + CLUSTER_FAILOVER, + clusterFailover: CLUSTER_FAILOVER, + CLUSTER_FLUSHSLOTS, + clusterFlushSlots: CLUSTER_FLUSHSLOTS, + CLUSTER_FORGET, + clusterForget: CLUSTER_FORGET, + CLUSTER_GETKEYSINSLOT, + clusterGetKeysInSlot: CLUSTER_GETKEYSINSLOT, + CLUSTER_INFO, + clusterInfo: CLUSTER_INFO, + CLUSTER_KEYSLOT, + clusterKeySlot: CLUSTER_KEYSLOT, + CLUSTER_LINKS, + clusterLinks: CLUSTER_LINKS, + CLUSTER_MEET, + clusterMeet: CLUSTER_MEET, + CLUSTER_MYID, + clusterMyId: CLUSTER_MYID, + CLUSTER_MYSHARDID, + clusterMyShardId: CLUSTER_MYSHARDID, + CLUSTER_NODES, + clusterNodes: CLUSTER_NODES, + CLUSTER_REPLICAS, + clusterReplicas: CLUSTER_REPLICAS, + CLUSTER_REPLICATE, + clusterReplicate: CLUSTER_REPLICATE, + CLUSTER_RESET, + clusterReset: CLUSTER_RESET, + CLUSTER_SAVECONFIG, + clusterSaveConfig: CLUSTER_SAVECONFIG, + 'CLUSTER_SET-CONFIG-EPOCH': CLUSTER_SET_CONFIG_EPOCH, + clusterSetConfigEpoch: CLUSTER_SET_CONFIG_EPOCH, + CLUSTER_SETSLOT, + clusterSetSlot: CLUSTER_SETSLOT, + CLUSTER_SLOTS, + clusterSlots: CLUSTER_SLOTS, + COMMAND_COUNT, + commandCount: COMMAND_COUNT, + COMMAND_GETKEYS, + commandGetKeys: COMMAND_GETKEYS, + COMMAND_GETKEYSANDFLAGS, + commandGetKeysAndFlags: COMMAND_GETKEYSANDFLAGS, + COMMAND_INFO, + commandInfo: COMMAND_INFO, + COMMAND_LIST, + commandList: COMMAND_LIST, + COMMAND, + command: COMMAND, + CONFIG_GET, + configGet: CONFIG_GET, + CONFIG_RESETASTAT, + configResetStat: CONFIG_RESETASTAT, + CONFIG_REWRITE, + configRewrite: CONFIG_REWRITE, + CONFIG_SET, + configSet: CONFIG_SET, + COPY, + copy: COPY, + DBSIZE, + dbSize: DBSIZE, + DECR, + decr: DECR, + DECRBY, + decrBy: DECRBY, + DEL, + del: DEL, + DUMP, + dump: DUMP, + ECHO, + echo: ECHO, + EVAL_RO, + evalRo: EVAL_RO, + EVAL, + eval: EVAL, + EVALSHA_RO, + evalShaRo: EVALSHA_RO, + EVALSHA, + evalSha: EVALSHA, + EXISTS, + exists: EXISTS, + EXPIRE, + expire: EXPIRE, + EXPIREAT, + expireAt: EXPIREAT, + EXPIRETIME, + expireTime: EXPIRETIME, + FLUSHALL, + flushAll: FLUSHALL, + FLUSHDB, + flushDb: FLUSHDB, + FCALL, + fCall: FCALL, + FCALL_RO, + fCallRo: FCALL_RO, + FUNCTION_DELETE, + functionDelete: FUNCTION_DELETE, + FUNCTION_DUMP, + functionDump: FUNCTION_DUMP, + FUNCTION_FLUSH, + functionFlush: FUNCTION_FLUSH, + FUNCTION_KILL, + functionKill: FUNCTION_KILL, + FUNCTION_LIST_WITHCODE, + functionListWithCode: FUNCTION_LIST_WITHCODE, + FUNCTION_LIST, + functionList: FUNCTION_LIST, + FUNCTION_LOAD, + functionLoad: FUNCTION_LOAD, + FUNCTION_RESTORE, + functionRestore: FUNCTION_RESTORE, + FUNCTION_STATS, + functionStats: FUNCTION_STATS, + GEOADD, + geoAdd: GEOADD, + GEODIST, + geoDist: GEODIST, + GEOHASH, + geoHash: GEOHASH, + GEOPOS, + geoPos: GEOPOS, + GEORADIUS_RO_WITH, + geoRadiusRoWith: GEORADIUS_RO_WITH, + GEORADIUS_RO, + geoRadiusRo: GEORADIUS_RO, + GEORADIUS_STORE, + geoRadiusStore: GEORADIUS_STORE, + GEORADIUS_WITH, + geoRadiusWith: GEORADIUS_WITH, + GEORADIUS, + geoRadius: GEORADIUS, + GEORADIUSBYMEMBER_RO_WITH, + geoRadiusByMemberRoWith: GEORADIUSBYMEMBER_RO_WITH, + GEORADIUSBYMEMBER_RO, + geoRadiusByMemberRo: GEORADIUSBYMEMBER_RO, + GEORADIUSBYMEMBER_STORE, + geoRadiusByMemberStore: GEORADIUSBYMEMBER_STORE, + GEORADIUSBYMEMBER_WITH, + geoRadiusByMemberWith: GEORADIUSBYMEMBER_WITH, + GEORADIUSBYMEMBER, + geoRadiusByMember: GEORADIUSBYMEMBER, + 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, + HEXPIRE, + hExpire: HEXPIRE, + HEXPIREAT, + hExpireAt: HEXPIREAT, + HEXPIRETIME, + hExpireTime: HEXPIRETIME, + HGET, + hGet: HGET, + HGETALL, + hGetAll: HGETALL, + HINCRBY, + hIncrBy: HINCRBY, + HINCRBYFLOAT, + hIncrByFloat: HINCRBYFLOAT, + HKEYS, + hKeys: HKEYS, + HLEN, + hLen: HLEN, + HMGET, + hmGet: HMGET, + HPERSIST, + hPersist: HPERSIST, + HPEXPIRE, + hpExpire: HPEXPIRE, + HPEXPIREAT, + hpExpireAt: HPEXPIREAT, + HPEXPIRETIME, + hpExpireTime: HPEXPIRETIME, + HPTTL, + hpTTL: HPTTL, + HRANDFIELD_COUNT_WITHVALUES, + hRandFieldCountWithValues: HRANDFIELD_COUNT_WITHVALUES, + HRANDFIELD_COUNT, + hRandFieldCount: HRANDFIELD_COUNT, + HRANDFIELD, + hRandField: HRANDFIELD, + HSCAN, + hScan: HSCAN, + HSCAN_NOVALUES, + hScanNoValues: HSCAN_NOVALUES, + HSET, + hSet: HSET, + HSETNX, + hSetNX: HSETNX, + HSTRLEN, + hStrLen: HSTRLEN, + HTTL, + hTTL: HTTL, + HVALS, + hVals: HVALS, + INCR, + incr: INCR, + INCRBY, + incrBy: INCRBY, + INCRBYFLOAT, + incrByFloat: INCRBYFLOAT, + INFO, + info: INFO, + KEYS, + keys: KEYS, + LASTSAVE, + lastSave: LASTSAVE, + LATENCY_DOCTOR, + latencyDoctor: LATENCY_DOCTOR, + LATENCY_GRAPH, + latencyGraph: LATENCY_GRAPH, + LATENCY_HISTORY, + latencyHistory: LATENCY_HISTORY, + LATENCY_LATEST, + latencyLatest: LATENCY_LATEST, + LCS_IDX_WITHMATCHLEN, + lcsIdxWithMatchLen: LCS_IDX_WITHMATCHLEN, + LCS_IDX, + lcsIdx: LCS_IDX, + LCS_LEN, + lcsLen: LCS_LEN, + LCS, + lcs: LCS, + LINDEX, + lIndex: LINDEX, + LINSERT, + lInsert: LINSERT, + LLEN, + lLen: LLEN, + LMOVE, + lMove: LMOVE, + LMPOP, + lmPop: LMPOP, + 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, + MEMORY_DOCTOR, + memoryDoctor: MEMORY_DOCTOR, + 'MEMORY_MALLOC-STATS': MEMORY_MALLOC_STATS, + memoryMallocStats: MEMORY_MALLOC_STATS, + MEMORY_PURGE, + memoryPurge: MEMORY_PURGE, + MEMORY_STATS, + memoryStats: MEMORY_STATS, + MEMORY_USAGE, + memoryUsage: MEMORY_USAGE, + 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, + OBJECT_ENCODING, + objectEncoding: OBJECT_ENCODING, + OBJECT_FREQ, + objectFreq: OBJECT_FREQ, + OBJECT_IDLETIME, + objectIdleTime: OBJECT_IDLETIME, + OBJECT_REFCOUNT, + objectRefCount: OBJECT_REFCOUNT, + PERSIST, + persist: PERSIST, + PEXPIRE, + pExpire: PEXPIRE, + PEXPIREAT, + pExpireAt: PEXPIREAT, + PEXPIRETIME, + pExpireTime: PEXPIRETIME, + PFADD, + pfAdd: PFADD, + PFCOUNT, + pfCount: PFCOUNT, + PFMERGE, + pfMerge: PFMERGE, + PING, + /** + * ping jsdoc + */ + 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, + PUBSUB_SHARDNUMSUB, + pubSubShardNumSub: PUBSUB_SHARDNUMSUB, + PUBSUB_SHARDCHANNELS, + pubSubShardChannels: PUBSUB_SHARDCHANNELS, + RANDOMKEY, + randomKey: RANDOMKEY, + READONLY, + readonly: READONLY, + RENAME, + rename: RENAME, + RENAMENX, + renameNX: RENAMENX, + REPLICAOF, + replicaOf: REPLICAOF, + 'RESTORE-ASKING': RESTORE_ASKING, + restoreAsking: RESTORE_ASKING, + RESTORE, + restore: RESTORE, + RPOP_COUNT, + rPopCount: RPOP_COUNT, + ROLE, + role: ROLE, + RPOP, + rPop: RPOP, + RPOPLPUSH, + rPopLPush: RPOPLPUSH, + RPUSH, + rPush: RPUSH, + RPUSHX, + rPushX: RPUSHX, + SADD, + sAdd: SADD, + 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, + SET, + set: SET, + SETBIT, + setBit: SETBIT, + SETEX, + setEx: SETEX, + SETNX, + setNX: SETNX, + SETRANGE, + setRange: SETRANGE, + SINTER, + sInter: SINTER, + SINTERCARD, + sInterCard: SINTERCARD, + SINTERSTORE, + sInterStore: SINTERSTORE, + SISMEMBER, + sIsMember: SISMEMBER, + SMEMBERS, + sMembers: SMEMBERS, + SMISMEMBER, + smIsMember: SMISMEMBER, + SMOVE, + sMove: SMOVE, + SORT_RO, + sortRo: SORT_RO, + SORT_STORE, + sortStore: SORT_STORE, + SORT, + sort: SORT, + SPOP_COUNT, + sPopCount: SPOP_COUNT, + SPOP, + sPop: SPOP, + SPUBLISH, + sPublish: SPUBLISH, + SRANDMEMBER_COUNT, + sRandMemberCount: SRANDMEMBER_COUNT, + SRANDMEMBER, + sRandMember: SRANDMEMBER, + SREM, + sRem: SREM, + SSCAN, + sScan: SSCAN, + STRLEN, + strLen: STRLEN, + SUNION, + sUnion: SUNION, + SUNIONSTORE, + sUnionStore: SUNIONSTORE, + SWAPDB, + swapDb: SWAPDB, + TIME, + time: TIME, + TOUCH, + touch: TOUCH, + TTL, + ttl: TTL, + TYPE, + type: TYPE, + UNLINK, + unlink: UNLINK, + WAIT, + wait: WAIT, + XACK, + xAck: XACK, + XADD_NOMKSTREAM, + xAddNoMkStream: XADD_NOMKSTREAM, + XADD, + xAdd: XADD, + XAUTOCLAIM_JUSTID, + xAutoClaimJustId: XAUTOCLAIM_JUSTID, + XAUTOCLAIM, + xAutoClaim: XAUTOCLAIM, + XCLAIM_JUSTID, + xClaimJustId: XCLAIM_JUSTID, + XCLAIM, + xClaim: XCLAIM, + XDEL, + xDel: XDEL, + XGROUP_CREATE, + xGroupCreate: XGROUP_CREATE, + XGROUP_CREATECONSUMER, + xGroupCreateConsumer: XGROUP_CREATECONSUMER, + XGROUP_DELCONSUMER, + xGroupDelConsumer: XGROUP_DELCONSUMER, + XGROUP_DESTROY, + xGroupDestroy: XGROUP_DESTROY, + XGROUP_SETID, + xGroupSetId: XGROUP_SETID, + XINFO_CONSUMERS, + xInfoConsumers: XINFO_CONSUMERS, + XINFO_GROUPS, + xInfoGroups: XINFO_GROUPS, + XINFO_STREAM, + xInfoStream: XINFO_STREAM, + XLEN, + xLen: XLEN, + XPENDING_RANGE, + xPendingRange: XPENDING_RANGE, + XPENDING, + xPending: XPENDING, + XRANGE, + xRange: XRANGE, + XREAD, + xRead: XREAD, + XREADGROUP, + xReadGroup: XREADGROUP, + XREVRANGE, + xRevRange: XREVRANGE, + XSETID, + xSetId: XSETID, + XTRIM, + xTrim: XTRIM, + ZADD_INCR, + zAddIncr: ZADD_INCR, + ZADD, + zAdd: ZADD, + ZCARD, + zCard: ZCARD, + ZCOUNT, + zCount: ZCOUNT, + ZDIFF_WITHSCORES, + zDiffWithScores: ZDIFF_WITHSCORES, + ZDIFF, + zDiff: ZDIFF, + ZDIFFSTORE, + zDiffStore: ZDIFFSTORE, + ZINCRBY, + zIncrBy: ZINCRBY, + ZINTER_WITHSCORES, + zInterWithScores: ZINTER_WITHSCORES, + ZINTER, + zInter: ZINTER, + ZINTERCARD, + zInterCard: ZINTERCARD, + ZINTERSTORE, + zInterStore: ZINTERSTORE, + ZLEXCOUNT, + zLexCount: ZLEXCOUNT, + ZMPOP, + zmPop: ZMPOP, + ZMSCORE, + zmScore: ZMSCORE, + ZPOPMAX_COUNT, + zPopMaxCount: ZPOPMAX_COUNT, + ZPOPMAX, + zPopMax: ZPOPMAX, + ZPOPMIN_COUNT, + zPopMinCount: ZPOPMIN_COUNT, + ZPOPMIN, + zPopMin: ZPOPMIN, + ZRANDMEMBER_COUNT_WITHSCORES, + zRandMemberCountWithScores: ZRANDMEMBER_COUNT_WITHSCORES, + ZRANDMEMBER_COUNT, + zRandMemberCount: ZRANDMEMBER_COUNT, + ZRANDMEMBER, + zRandMember: ZRANDMEMBER, + ZRANGE_WITHSCORES, + zRangeWithScores: ZRANGE_WITHSCORES, + ZRANGE, + zRange: ZRANGE, + ZRANGEBYLEX, + zRangeByLex: ZRANGEBYLEX, + ZRANGEBYSCORE_WITHSCORES, + zRangeByScoreWithScores: ZRANGEBYSCORE_WITHSCORES, + ZRANGEBYSCORE, + zRangeByScore: ZRANGEBYSCORE, + ZRANGESTORE, + zRangeStore: ZRANGESTORE, + ZRANK_WITHSCORE, + zRankWithScore: ZRANK_WITHSCORE, + 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 +} as const satisfies RedisCommands; diff --git a/packages/client/lib/errors.ts b/packages/client/lib/errors.ts index aa97d9cf26d..8af4c5e5bed 100644 --- a/packages/client/lib/errors.ts +++ b/packages/client/lib/errors.ts @@ -1,84 +1,88 @@ -import { RedisCommandRawReply } from './commands'; - export class AbortError extends Error { - constructor() { - super('The command was aborted'); - } + constructor() { + super('The command was aborted'); + } } export class WatchError extends Error { - constructor() { - super('One (or more) of the watched keys has been changed'); - } + constructor(message = 'One (or more) of the watched keys has been changed') { + super(message); + } } export class ConnectionTimeoutError extends Error { - constructor() { - super('Connection timeout'); - } + constructor() { + super('Connection timeout'); + } } export class ClientClosedError extends Error { - constructor() { - super('The client is closed'); - } + constructor() { + super('The client is closed'); + } } export class ClientOfflineError extends Error { - constructor() { - super('The client is offline'); - } + constructor() { + super('The client is offline'); + } } export class DisconnectsClientError extends Error { - constructor() { - super('Disconnects client'); - } + constructor() { + super('Disconnects client'); + } } export class SocketClosedUnexpectedlyError extends Error { - constructor() { - super('Socket closed unexpectedly'); - } + constructor() { + super('Socket closed unexpectedly'); + } } export class RootNodesUnavailableError extends Error { - constructor() { - super('All the root nodes are unavailable'); - } + constructor() { + super('All the root nodes are unavailable'); + } } export class ReconnectStrategyError extends Error { - originalError: Error; - socketError: unknown; + originalError: Error; + socketError: unknown; - constructor(originalError: Error, socketError: unknown) { - super(originalError.message); - this.originalError = originalError; - this.socketError = socketError; - } + constructor(originalError: Error, socketError: unknown) { + super(originalError.message); + this.originalError = originalError; + this.socketError = socketError; + } } export class ErrorReply extends Error { - constructor(message: string) { - super(message); - this.stack = undefined; - } + constructor(message: string) { + super(message); + this.stack = undefined; + } } +export class SimpleError extends ErrorReply {} + +export class BlobError extends ErrorReply {} + +export class TimeoutError extends Error {} + export class MultiErrorReply extends ErrorReply { - replies; - errorIndexes; + replies: Array; + errorIndexes: Array; - constructor(replies: Array, errorIndexes: Array) { - super(`${errorIndexes.length} commands failed, see .replies and .errorIndexes for more information`); - this.replies = replies; - this.errorIndexes = errorIndexes; - } + constructor(replies: Array, errorIndexes: Array) { + super(`${errorIndexes.length} commands failed, see .replies and .errorIndexes for more information`); + this.replies = replies; + this.errorIndexes = errorIndexes; + } - *errors() { - for (const index of this.errorIndexes) { - yield this.replies[index]; - } + *errors() { + for (const index of this.errorIndexes) { + yield this.replies[index]; } + } } diff --git a/packages/client/lib/lua-script.ts b/packages/client/lib/lua-script.ts index da19417ec25..6d395b71232 100644 --- a/packages/client/lib/lua-script.ts +++ b/packages/client/lib/lua-script.ts @@ -1,22 +1,22 @@ -import { createHash } from 'crypto'; -import { RedisCommand } from './commands'; +import { createHash } from 'node:crypto'; +import { Command } from './RESP/types'; -export interface RedisScriptConfig extends RedisCommand { - SCRIPT: string; - NUMBER_OF_KEYS?: number; +export type RedisScriptConfig = Command & { + SCRIPT: string | Buffer; + NUMBER_OF_KEYS?: number; } export interface SHA1 { - SHA1: string; + SHA1: string; } export function defineScript(script: S): S & SHA1 { - return { - ...script, - SHA1: scriptSha1(script.SCRIPT) - }; + return { + ...script, + SHA1: scriptSha1(script.SCRIPT) + }; } -export function scriptSha1(script: string): string { - return createHash('sha1').update(script).digest('hex'); +export function scriptSha1(script: RedisScriptConfig['SCRIPT']): string { + return createHash('sha1').update(script).digest('hex'); } diff --git a/packages/client/lib/multi-command.spec.ts b/packages/client/lib/multi-command.spec.ts index b0f79c6e157..7e77f88d10b 100644 --- a/packages/client/lib/multi-command.spec.ts +++ b/packages/client/lib/multi-command.spec.ts @@ -1,96 +1,77 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import RedisMultiCommand from './multi-command'; -import { WatchError } from './errors'; import { SQUARE_SCRIPT } from './client/index.spec'; describe('Multi Command', () => { - it('generateChainId', () => { - assert.equal( - typeof RedisMultiCommand.generateChainId(), - 'symbol' - ); - }); - - it('addCommand', () => { - const multi = new RedisMultiCommand(); - multi.addCommand(['PING']); - - assert.deepEqual( - multi.queue[0].args, - ['PING'] - ); - }); + it('addCommand', () => { + const multi = new RedisMultiCommand(); + multi.addCommand(['PING']); - it('addScript', () => { - const multi = new RedisMultiCommand(); + assert.deepEqual( + multi.queue[0].args, + ['PING'] + ); + }); - multi.addScript(SQUARE_SCRIPT, ['1']); - assert.equal( - multi.scriptsInUse.has(SQUARE_SCRIPT.SHA1), - true - ); - assert.deepEqual( - multi.queue[0].args, - ['EVAL', SQUARE_SCRIPT.SCRIPT, '0', '1'] - ); + describe('addScript', () => { + const multi = new RedisMultiCommand(); - multi.addScript(SQUARE_SCRIPT, ['2']); - assert.equal( - multi.scriptsInUse.has(SQUARE_SCRIPT.SHA1), - true - ); - assert.deepEqual( - multi.queue[1].args, - ['EVALSHA', SQUARE_SCRIPT.SHA1, '0', '2'] - ); + it('should use EVAL', () => { + multi.addScript(SQUARE_SCRIPT, ['1']); + assert.deepEqual( + Array.from(multi.queue.at(-1).args), + ['EVAL', SQUARE_SCRIPT.SCRIPT, '1', '1'] + ); }); - describe('exec', () => { - it('without commands', () => { - assert.deepEqual( - new RedisMultiCommand().queue, - [] - ); - }); + it('should use EVALSHA', () => { + multi.addScript(SQUARE_SCRIPT, ['2']); + assert.deepEqual( + Array.from(multi.queue.at(-1).args), + ['EVALSHA', SQUARE_SCRIPT.SHA1, '1', '2'] + ); + }); - it('with commands', () => { - const multi = new RedisMultiCommand(); - multi.addCommand(['PING']); + it('without NUMBER_OF_KEYS', () => { + multi.addScript({ + ...SQUARE_SCRIPT, + NUMBER_OF_KEYS: undefined + }, ['2']); + assert.deepEqual( + Array.from(multi.queue.at(-1).args), + ['EVALSHA', SQUARE_SCRIPT.SHA1, '2'] + ); + }); + }); - assert.deepEqual( - multi.queue, - [{ - args: ['PING'], - transformReply: undefined - }] - ); - }); + describe('exec', () => { + it('without commands', () => { + assert.deepEqual( + new RedisMultiCommand().queue, + [] + ); }); - describe('handleExecReplies', () => { - it('WatchError', () => { - assert.throws( - () => new RedisMultiCommand().handleExecReplies([null]), - WatchError - ); - }); + it('with commands', () => { + const multi = new RedisMultiCommand(); + multi.addCommand(['PING']); - it('with replies', () => { - const multi = new RedisMultiCommand(); - multi.addCommand(['PING']); - assert.deepEqual( - multi.handleExecReplies(['OK', 'QUEUED', ['PONG']]), - ['PONG'] - ); - }); + assert.deepEqual( + multi.queue, + [{ + args: ['PING'], + transformReply: undefined + }] + ); }); + }); - it('transformReplies', () => { - const multi = new RedisMultiCommand(); - multi.addCommand(['PING'], (reply: string) => reply.substring(0, 2)); - assert.deepEqual( - multi.transformReplies(['PONG']), - ['PO'] - ); - }); + it('transformReplies', () => { + const multi = new RedisMultiCommand(); + multi.addCommand(['PING'], (reply: string) => reply.substring(0, 2)); + assert.deepEqual( + multi.transformReplies(['PONG']), + ['PO'] + ); + }); }); diff --git a/packages/client/lib/multi-command.ts b/packages/client/lib/multi-command.ts index 642c2ea36c0..a3ff4c99407 100644 --- a/packages/client/lib/multi-command.ts +++ b/packages/client/lib/multi-command.ts @@ -1,95 +1,64 @@ -import { fCallArguments } from './commander'; -import { RedisCommand, RedisCommandArguments, RedisCommandRawReply, RedisFunction, RedisScript } from './commands'; -import { ErrorReply, MultiErrorReply, WatchError } from './errors'; +import { CommandArguments, RedisScript, ReplyUnion, TransformReply, TypeMapping } from './RESP/types'; +import { ErrorReply, MultiErrorReply } from './errors'; + +export type MULTI_REPLY = { + GENERIC: 'generic'; + TYPED: 'typed'; +}; + +export type MultiReply = MULTI_REPLY[keyof MULTI_REPLY]; + +export type MultiReplyType = T extends MULTI_REPLY['TYPED'] ? REPLIES : Array; export interface RedisMultiQueuedCommand { - args: RedisCommandArguments; - transformReply?: RedisCommand['transformReply']; + args: CommandArguments; + transformReply?: TransformReply; } export default class RedisMultiCommand { - static generateChainId(): symbol { - return Symbol('RedisMultiCommand Chain Id'); - } + readonly queue: Array = []; - readonly queue: Array = []; + readonly scriptsInUse = new Set(); - readonly scriptsInUse = new Set(); + addCommand(args: CommandArguments, transformReply?: TransformReply) { + this.queue.push({ + args, + transformReply + }); + } - addCommand(args: RedisCommandArguments, transformReply?: RedisCommand['transformReply']): void { - this.queue.push({ - args, - transformReply - }); + addScript(script: RedisScript, args: CommandArguments, transformReply?: TransformReply) { + const redisArgs: CommandArguments = []; + redisArgs.preserve = args.preserve; + if (this.scriptsInUse.has(script.SHA1)) { + redisArgs.push('EVALSHA', script.SHA1); + } else { + this.scriptsInUse.add(script.SHA1); + redisArgs.push('EVAL', script.SCRIPT); } - addFunction(name: string, fn: RedisFunction, args: Array): RedisCommandArguments { - const transformedArguments = fCallArguments( - name, - fn, - fn.transformArguments(...args) - ); - this.queue.push({ - args: transformedArguments, - transformReply: fn.transformReply - }); - return transformedArguments; + if (script.NUMBER_OF_KEYS !== undefined) { + redisArgs.push(script.NUMBER_OF_KEYS.toString()); } - addScript(script: RedisScript, args: Array): RedisCommandArguments { - const transformedArguments: RedisCommandArguments = []; - if (this.scriptsInUse.has(script.SHA1)) { - transformedArguments.push( - 'EVALSHA', - script.SHA1 - ); - } else { - this.scriptsInUse.add(script.SHA1); - transformedArguments.push( - 'EVAL', - script.SCRIPT - ); - } - - if (script.NUMBER_OF_KEYS !== undefined) { - transformedArguments.push(script.NUMBER_OF_KEYS.toString()); - } + redisArgs.push(...args); - const scriptArguments = script.transformArguments(...args); - transformedArguments.push(...scriptArguments); - if (scriptArguments.preserve) { - transformedArguments.preserve = scriptArguments.preserve; + this.addCommand(redisArgs, transformReply); + } + + transformReplies(rawReplies: Array, typeMapping?: TypeMapping): Array { + const errorIndexes: Array = [], + replies = rawReplies.map((reply, i) => { + if (reply instanceof ErrorReply) { + errorIndexes.push(i); + return reply; } - this.addCommand( - transformedArguments, - script.transformReply - ); + const { transformReply, args } = this.queue[i]; + return transformReply ? transformReply(reply, args.preserve, typeMapping) : reply; + }); - return transformedArguments; - } - - handleExecReplies(rawReplies: Array): Array { - const execReply = rawReplies[rawReplies.length - 1] as (null | Array); - if (execReply === null) { - throw new WatchError(); - } - - return this.transformReplies(execReply); - } - - transformReplies(rawReplies: Array): Array { - const errorIndexes: Array = [], - replies = rawReplies.map((reply, i) => { - if (reply instanceof ErrorReply) { - errorIndexes.push(i); - return reply; - } - const { transformReply, args } = this.queue[i]; - return transformReply ? transformReply(reply, args.preserve) : reply; - }); - - if (errorIndexes.length) throw new MultiErrorReply(replies, errorIndexes); - return replies; - } + if (errorIndexes.length) throw new MultiErrorReply(replies, errorIndexes); + return replies; + } } diff --git a/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts b/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts new file mode 100644 index 00000000000..b260dcfba7d --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts @@ -0,0 +1,12 @@ +import { RedisArgument, MapReply, BlobStringReply, Command } from '../../RESP/types'; +import { transformTuplesReply } from '../../commands/generic-transformers'; + +export default { + transformArguments(dbname: RedisArgument) { + return ['SENTINEL', 'MASTER', dbname]; + }, + transformReply: { + 2: transformTuplesReply, + 3: undefined as unknown as () => MapReply + } +} as const satisfies Command; diff --git a/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts b/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts new file mode 100644 index 00000000000..14caecd924a --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts @@ -0,0 +1,8 @@ +import { RedisArgument, SimpleStringReply, Command } from '../../RESP/types'; + +export default { + transformArguments(dbname: RedisArgument, host: RedisArgument, port: RedisArgument, quorum: RedisArgument) { + return ['SENTINEL', 'MONITOR', dbname, host, port, quorum]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts b/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts new file mode 100644 index 00000000000..3d002896355 --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts @@ -0,0 +1,23 @@ +import { RedisArgument, ArrayReply, BlobStringReply, MapReply, Command, TypeMapping, UnwrapReply } from '../../RESP/types'; +import { transformTuplesReply } from '../../commands/generic-transformers'; + +export default { + transformArguments(dbname: RedisArgument) { + return ['SENTINEL', 'REPLICAS', dbname]; + }, + transformReply: { + 2: (reply: ArrayReply>, preserve?: any, typeMapping?: TypeMapping) => { + const inferred = reply as unknown as UnwrapReply; + const initial: Array> = []; + + return inferred.reduce( + (sentinels: Array>, x: ArrayReply) => { + sentinels.push(transformTuplesReply(x, undefined, typeMapping)); + return sentinels; + }, + initial + ); + }, + 3: undefined as unknown as () => ArrayReply> + } +} as const satisfies Command; diff --git a/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts b/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts new file mode 100644 index 00000000000..22c1e0123fc --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts @@ -0,0 +1,23 @@ +import { RedisArgument, ArrayReply, MapReply, BlobStringReply, Command, TypeMapping, UnwrapReply } from '../../RESP/types'; +import { transformTuplesReply } from '../../commands/generic-transformers'; + +export default { + transformArguments(dbname: RedisArgument) { + return ['SENTINEL', 'SENTINELS', dbname]; + }, + transformReply: { + 2: (reply: ArrayReply>, preserve?: any, typeMapping?: TypeMapping) => { + const inferred = reply as unknown as UnwrapReply; + const initial: Array> = []; + + return inferred.reduce( + (sentinels: Array>, x: ArrayReply) => { + sentinels.push(transformTuplesReply(x, undefined, typeMapping)); + return sentinels; + }, + initial + ); + }, + 3: undefined as unknown as () => ArrayReply> + } +} as const satisfies Command; diff --git a/packages/client/lib/sentinel/commands/SENTINEL_SET.ts b/packages/client/lib/sentinel/commands/SENTINEL_SET.ts new file mode 100644 index 00000000000..41037819869 --- /dev/null +++ b/packages/client/lib/sentinel/commands/SENTINEL_SET.ts @@ -0,0 +1,19 @@ +import { RedisArgument, SimpleStringReply, Command } from '../../RESP/types'; + +export type SentinelSetOptions = Array<{ + option: RedisArgument; + value: RedisArgument; +}>; + +export default { + transformArguments(dbname: RedisArgument, options: SentinelSetOptions) { + const args = ['SENTINEL', 'SET', dbname]; + + for (const option of options) { + args.push(option.option, option.value); + } + + return args; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/client/lib/sentinel/commands/index.ts b/packages/client/lib/sentinel/commands/index.ts new file mode 100644 index 00000000000..1fc16f872f6 --- /dev/null +++ b/packages/client/lib/sentinel/commands/index.ts @@ -0,0 +1,19 @@ +import { RedisCommands } from '../../RESP/types'; +import SENTINEL_MASTER from './SENTINEL_MASTER'; +import SENTINEL_MONITOR from './SENTINEL_MONITOR'; +import SENTINEL_REPLICAS from './SENTINEL_REPLICAS'; +import SENTINEL_SENTINELS from './SENTINEL_SENTINELS'; +import SENTINEL_SET from './SENTINEL_SET'; + +export default { + SENTINEL_SENTINELS, + sentinelSentinels: SENTINEL_SENTINELS, + SENTINEL_MASTER, + sentinelMaster: SENTINEL_MASTER, + SENTINEL_REPLICAS, + sentinelReplicas: SENTINEL_REPLICAS, + SENTINEL_MONITOR, + sentinelMonitor: SENTINEL_MONITOR, + SENTINEL_SET, + sentinelSet: SENTINEL_SET +} as const satisfies RedisCommands; diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts new file mode 100644 index 00000000000..1fba8d6b42f --- /dev/null +++ b/packages/client/lib/sentinel/index.spec.ts @@ -0,0 +1,1276 @@ +import { strict as assert } from 'node:assert'; +import { setTimeout } from 'node:timers/promises'; +import { WatchError } from "../errors"; +import { RedisSentinelConfig, SentinelFramework } from "./test-util"; +import { RedisNode, RedisSentinelClientType, RedisSentinelEvent, RedisSentinelType } from "./types"; +import { RedisSentinelFactory } from '.'; +import { RedisClientType } from '../client'; +import { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, NumberReply } from '../RESP/types'; + +import { promisify } from 'node:util'; +import { exec } from 'node:child_process'; +import { RESP_TYPES } from '../RESP/decoder'; +import { defineScript } from '../lua-script'; +import { MATH_FUNCTION } from '../commands/FUNCTION_LOAD.spec'; +import RedisBloomModules from '@redis/bloom'; +import { RedisTcpSocketOptions } from '../client/socket'; + +const execAsync = promisify(exec); + +const SQUARE_SCRIPT = defineScript({ + SCRIPT: + `local number = redis.call('GET', KEYS[1]) + return number * number`, + NUMBER_OF_KEYS: 1, + FIRST_KEY_INDEX: 0, + transformArguments(key: string) { + return [key]; + }, + transformReply: undefined as unknown as () => NumberReply +}); + +/* used to ensure test environment resets to normal state + i.e. + - all redis nodes are active and are part of the topology + before allowing things to continue. +*/ + +async function steadyState(frame: SentinelFramework) { + let checkedMaster = false; + let checkedReplicas = false; + while (!checkedMaster || !checkedReplicas) { + if (!checkedMaster) { + const master = await frame.sentinelMaster(); + if (master?.flags === 'master') { + checkedMaster = true; + } + } + + if (!checkedReplicas) { + const replicas = (await frame.sentinelReplicas()); + checkedReplicas = true; + for (const replica of replicas!) { + checkedReplicas &&= (replica.flags === 'slave'); + } + } + } + + let nodeResolve, nodeReject; + const nodePromise = new Promise((res, rej) => { + nodeResolve = res; + nodeReject = rej; + }) + + const seenNodes = new Set(); + let sentinel: RedisSentinelType | undefined; + const tracer = []; + + try { + sentinel = frame.getSentinelClient({ replicaPoolSize: 1, scanInterval: 2000 }, false) + .on('topology-change', (event: RedisSentinelEvent) => { + if (event.type == "MASTER_CHANGE" || event.type == "REPLICA_ADD") { + seenNodes.add(event.node.port); + if (seenNodes.size == frame.getAllNodesPort().length) { + nodeResolve(); + } + } + }).on('error', err => { }); + sentinel.setTracer(tracer); + await sentinel.connect(); + + await nodePromise; + + await sentinel.flushAll(); + } finally { + if (sentinel !== undefined) { + sentinel.destroy(); + } + } +} + +["redis-sentinel-test-password", undefined].forEach(function (password) { + describe.skip(`Sentinel - password = ${password}`, () => { + const config: RedisSentinelConfig = { sentinelName: "test", numberOfNodes: 3, password: password }; + const frame = new SentinelFramework(config); + let tracer = new Array(); + let stopMeasuringBlocking = false; + let longestDelta = 0; + let longestTestDelta = 0; + let last: number; + + before(async function () { + this.timeout(15000); + + last = Date.now(); + + function deltaMeasurer() { + const delta = Date.now() - last; + if (delta > longestDelta) { + longestDelta = delta; + } + if (delta > longestTestDelta) { + longestTestDelta = delta; + } + if (!stopMeasuringBlocking) { + last = Date.now(); + setImmediate(deltaMeasurer); + } + } + + setImmediate(deltaMeasurer); + + await frame.spawnRedisSentinel(); + }); + + after(async function () { + this.timeout(15000); + + stopMeasuringBlocking = true; + + await frame.cleanup(); + }) + + describe('Sentinel Client', function () { + let sentinel: RedisSentinelType< RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> | undefined; + + beforeEach(async function () { + this.timeout(0); + + await frame.getAllRunning(); + + await steadyState(frame); + longestTestDelta = 0; + }) + + afterEach(async function () { + this.timeout(30000); + + // avoid errors in afterEach that end testing + if (sentinel !== undefined) { + sentinel.on('error', () => { }); + } + + if (this!.currentTest!.state === 'failed') { + console.log(`longest event loop blocked delta: ${longestDelta}`); + console.log(`longest event loop blocked in failing test: ${longestTestDelta}`); + console.log("trace:"); + for (const line of tracer) { + console.log(line); + } + console.log(`sentinel object state:`) + console.log(`master: ${JSON.stringify(sentinel?.getMasterNode())}`) + console.log(`replicas: ${JSON.stringify(sentinel?.getReplicaNodes().entries)}`) + const results = await Promise.all([ + frame.sentinelSentinels(), + frame.sentinelMaster(), + frame.sentinelReplicas() + ]) + console.log(`sentinel sentinels:\n${JSON.stringify(results[0], undefined, '\t')}`); + console.log(`sentinel master:\n${JSON.stringify(results[1], undefined, '\t')}`); + console.log(`sentinel replicas:\n${JSON.stringify(results[2], undefined, '\t')}`); + const { stdout, stderr } = await execAsync("docker ps -a"); + console.log(`docker stdout:\n${stdout}`); + + const ids = frame.getAllDockerIds(); + console.log("docker logs"); + + for (const [id, port] of ids) { + console.log(`${id}/${port}\n`); + const { stdout, stderr } = await execAsync(`docker logs ${id}`, {maxBuffer: 8192 * 8192 * 4}); + console.log(stdout); + } + } + tracer.length = 0; + + if (sentinel !== undefined) { + await sentinel.destroy(); + sentinel = undefined; + } + }) + + it('basic bootstrap', async function () { + sentinel = frame.getSentinelClient(); + await sentinel.connect(); + + await assert.doesNotReject(sentinel.set('x', 1)); + + }); + + it('basic teardown worked', async function () { + const nodePorts = frame.getAllNodesPort(); + const sentinelPorts = frame.getAllSentinelsPort(); + + assert.notEqual(nodePorts.length, 0); + assert.notEqual(sentinelPorts.length, 0); + + sentinel = frame.getSentinelClient(); + await sentinel.connect(); + + await assert.doesNotReject(sentinel.get('x')); + }); + + it('try to connect multiple times', async function () { + sentinel = frame.getSentinelClient(); + const connectPromise = sentinel.connect(); + await assert.rejects(sentinel.connect()); + await connectPromise; + }); + + it('with type mapping', async function () { + const commandOptions = { + typeMapping: { + [RESP_TYPES.SIMPLE_STRING]: Buffer + } + } + sentinel = frame.getSentinelClient({ commandOptions: commandOptions }); + await sentinel.connect(); + + const resp = await sentinel.ping(); + assert.deepEqual(resp, Buffer.from('PONG')) + }) + + it('with a script', async function () { + const options = { + scripts: { + square: SQUARE_SCRIPT + } + } + + sentinel = frame.getSentinelClient(options); + await sentinel.connect(); + + const [, reply] = await Promise.all([ + sentinel.set('key', '2'), + sentinel.square('key') + ]); + + assert.equal(reply, 4); + }) + + it('multi with a script', async function () { + const options = { + scripts: { + square: SQUARE_SCRIPT + } + } + + sentinel = frame.getSentinelClient(options); + await sentinel.connect(); + + const reply = await sentinel.multi().set('key', 2).square('key').exec(); + + assert.deepEqual(reply, ['OK', 4]); + }) + + it('with a function', async function () { + const options = { + functions: { + math: MATH_FUNCTION.library + } + } + sentinel = frame.getSentinelClient(options); + await sentinel.connect(); + + await sentinel.functionLoad( + MATH_FUNCTION.code, + { REPLACE: true } + ); + + await sentinel.set('key', '2'); + const resp = await sentinel.math.square('key'); + + assert.equal(resp, 4); + }) + + it('multi with a function', async function () { + const options = { + functions: { + math: MATH_FUNCTION.library + } + } + sentinel = frame.getSentinelClient(options); + await sentinel.connect(); + + await sentinel.functionLoad( + MATH_FUNCTION.code, + { REPLACE: true } + ); + + const reply = await sentinel.multi().set('key', 2).math.square('key').exec(); + assert.deepEqual(reply, ['OK', 4]); + }) + + it('with a module', async function () { + const options = { + modules: RedisBloomModules + } + sentinel = frame.getSentinelClient(options); + await sentinel.connect(); + + const resp = await sentinel.bf.add('key', 'item') + assert.equal(resp, true); + }) + + it('multi with a module', async function () { + const options = { + modules: RedisBloomModules + } + sentinel = frame.getSentinelClient(options); + await sentinel.connect(); + + const resp = await sentinel.multi().bf.add('key', 'item').exec(); + assert.deepEqual(resp, [true]); + }) + + it('many readers', async function () { + this.timeout(10000); + + sentinel = frame.getSentinelClient({ replicaPoolSize: 8 }); + await sentinel.connect(); + + await sentinel.set("x", 1); + for (let i = 0; i < 10; i++) { + if (await sentinel.get("x") == "1") { + break; + } + await setTimeout(1000); + } + + const promises: Array> = []; + for (let i = 0; i < 500; i++) { + promises.push(sentinel.get("x")); + } + + const resp = await Promise.all(promises); + assert.equal(resp.length, 500); + for (let i = 0; i < 500; i++) { + assert.equal(resp[i], "1", `failed on match at ${i}`); + } + }); + + it('use', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); + sentinel.on("error", () => { }); + await sentinel.connect(); + + await sentinel.use( + async (client: RedisSentinelClientType, ) => { + const masterNode = sentinel!.getMasterNode(); + await frame.stopNode(masterNode!.port.toString()); + await assert.doesNotReject(client.get('x')); + } + ); + }); + + it('use with script', async function () { + this.timeout(10000); + + const options = { + scripts: { + square: SQUARE_SCRIPT + } + } + + sentinel = frame.getSentinelClient(options); + await sentinel.connect(); + + const reply = await sentinel.use( + async (client: RedisSentinelClientType) => { + assert.equal(await client.set('key', '2'), 'OK'); + assert.equal(await client.get('key'), '2'); + return client.square('key') + } + ); + + assert.equal(reply, 4); + }) + + it('use with a function', async function () { + this.timeout(10000); + + const options = { + functions: { + math: MATH_FUNCTION.library + } + } + sentinel = frame.getSentinelClient(options); + await sentinel.connect(); + + await sentinel.functionLoad( + MATH_FUNCTION.code, + { REPLACE: true } + ); + + const reply = await sentinel.use( + async (client: RedisSentinelClientType) => { + await client.set('key', '2'); + return client.math.square('key'); + } + ); + + assert.equal(reply, 4); + }) + + it('use with a module', async function () { + const options = { + modules: RedisBloomModules + } + sentinel = frame.getSentinelClient(options); + await sentinel.connect(); + + const reply = await sentinel.use( + async (client: RedisSentinelClientType) => { + return client.bf.add('key', 'item'); + } + ); + + assert.equal(reply, true); + }) + + it('block on pool', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); + sentinel.on("error", () => { }); + await sentinel.connect(); + + const promise = sentinel.use( + async client => { + await setTimeout(1000); + return await client.get("x"); + } + ) + + await sentinel.set("x", 1); + assert.equal(await promise, null); + }); + + it('reserve client, takes a client out of pool', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ masterPoolSize: 2, reserveClient: true }); + await sentinel.connect(); + + const promise1 = sentinel.use( + async client => { + const val = await client.get("x"); + await client.set("x", 2); + return val; + } + ) + + const promise2 = sentinel.use( + async client => { + return client.get("x"); + } + ) + + await sentinel.set("x", 1); + assert.equal(await promise1, "1"); + assert.equal(await promise2, "2"); + }) + + it('multiple clients', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + sentinel.on("error", () => { }); + await sentinel.connect(); + + let set = false; + + const promise = sentinel.use( + async client => { + await sentinel!.set("x", 1); + await client.get("x"); + } + ) + + await assert.doesNotReject(promise); + }); + + // by taking a lease, we know we will block on master as no clients are available, but as read occuring, means replica read occurs + it('replica reads', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); + sentinel.on("error", () => { }); + await sentinel.connect(); + + const clientLease = await sentinel.aquire(); + clientLease.set('x', 456); + + let matched = false; + /* waits for replication */ + for (let i = 0; i < 15; i++) { + try { + assert.equal(await sentinel.get("x"), '456'); + matched = true; + break; + } catch (err) { + await setTimeout(1000); + } + } + + clientLease.release(); + + assert.equal(matched, true); + }); + + it('pipeline', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); + await sentinel.connect(); + + const resp = await sentinel.multi().set('x', 1).get('x').execAsPipeline(); + + assert.deepEqual(resp, ['OK', '1']); + }) + + it('use - watch - clean', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + await sentinel.connect(); + + let promise = sentinel.use(async (client) => { + await client.set("x", 1); + await client.watch("x"); + return client.multi().get("x").exec(); + }); + + assert.deepEqual(await promise, ['1']); + }); + + it('use - watch - dirty', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + await sentinel.connect(); + + let promise = sentinel.use(async (client) => { + await client.set('x', 1); + await client.watch('x'); + await sentinel!.set('x', 2); + return client.multi().get('x').exec(); + }); + + await assert.rejects(promise, new WatchError()); + }); + + it('lease - watch - clean', async function () { + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + await sentinel.connect(); + + const leasedClient = await sentinel.aquire(); + await leasedClient.set('x', 1); + await leasedClient.watch('x'); + assert.deepEqual(await leasedClient.multi().get('x').exec(), ['1']) + }); + + it('lease - watch - dirty', async function () { + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + await sentinel.connect(); + + const leasedClient = await sentinel.aquire(); + await leasedClient.set('x', 1); + await leasedClient.watch('x'); + await leasedClient.set('x', 2); + + await assert.rejects(leasedClient.multi().get('x').exec(), new WatchError()); + }); + + + it('watch does not carry through leases', async function () { + this.timeout(10000); + sentinel = frame.getSentinelClient(); + await sentinel.connect(); + + // each of these commands is an independent lease + assert.equal(await sentinel.use(client => client.watch("x")), 'OK') + assert.equal(await sentinel.use(client => client.set('x', 1)), 'OK'); + assert.deepEqual(await sentinel.use(client => client.multi().get('x').exec()), ['1']); + }); + + // stops master to force sentinel to update + it('stop master', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + + tracer.push(`connected`); + + let masterChangeResolve; + const masterChangePromise = new Promise((res) => { + masterChangeResolve = res; + }) + + const masterNode = await sentinel.getMasterNode(); + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push(`got expected master change event`); + masterChangeResolve(event.node); + } + }); + + tracer.push(`stopping master node`); + await frame.stopNode(masterNode!.port.toString()); + tracer.push(`stopped master node`); + + tracer.push(`waiting on master change promise`); + const newMaster = await masterChangePromise as RedisNode; + tracer.push(`got new master node of ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); + }); + + // if master changes, client should make sure user knows watches are invalid + it('watch across master change', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + + tracer.push("connected"); + + const client = await sentinel.aquire(); + tracer.push("aquired lease"); + + await client.set("x", 1); + await client.watch("x"); + + tracer.push("did a watch on lease"); + + let resolve; + const promise = new Promise((res) => { + resolve = res; + }) + + const masterNode = sentinel.getMasterNode(); + tracer.push(`got masterPort as ${masterNode!.port}`); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push("resolving promise"); + resolve(event.node); + } + }); + + tracer.push("stopping master node"); + await frame.stopNode(masterNode!.port.toString()); + tracer.push("stopped master node and waiting on promise"); + + const newMaster = await promise as RedisNode; + tracer.push(`promise returned, newMaster = ${JSON.stringify(newMaster)}`); + assert.notEqual(masterNode!.port, newMaster.port); + tracer.push(`newMaster does not equal old master`); + + tracer.push(`waiting to assert that a multi/exec now fails`); + await assert.rejects(async () => { await client.multi().get("x").exec() }, new Error("sentinel config changed in middle of a WATCH Transaction")); + tracer.push(`asserted that a multi/exec now fails`); + }); + + // same as above, but set a watch before and after master change, shouldn't change the fact that watches are invalid + it('watch before and after master change', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + tracer.push("connected"); + + const client = await sentinel.aquire(); + tracer.push("got leased client"); + await client.set("x", 1); + await client.watch("x"); + + tracer.push("set and watched x"); + + let resolve; + const promise = new Promise((res) => { + resolve = res; + }) + + const masterNode = sentinel.getMasterNode(); + tracer.push(`initial masterPort = ${masterNode!.port} `); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push("got a master change event that is not the same as before"); + resolve(event.node); + } + }); + + tracer.push("stopping master"); + await frame.stopNode(masterNode!.port.toString()); + tracer.push("stopped master"); + + tracer.push("waiting on master change promise"); + const newMaster = await promise as RedisNode; + tracer.push(`got master change port as ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); + + tracer.push("watching again, shouldn't matter"); + await client.watch("y"); + + tracer.push("expecting multi to be rejected"); + await assert.rejects(async () => { await client.multi().get("x").exec() }, new Error("sentinel config changed in middle of a WATCH Transaction")); + tracer.push("multi was rejected"); + }); + + it('plain pubsub - channel', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + await sentinel.connect(); + tracer.push(`connected`); + + let pubSubResolve; + const pubSubPromise = new Promise((res) => { + pubSubResolve = res; + }); + + let tester = false; + await sentinel.subscribe('test', () => { + tracer.push(`got pubsub message`); + tester = true; + pubSubResolve(1); + }) + + tracer.push(`publishing pubsub message`); + await sentinel.publish('test', 'hello world'); + tracer.push(`waiting on pubsub promise`); + await pubSubPromise; + tracer.push(`got pubsub promise`); + assert.equal(tester, true); + + // now unsubscribe + tester = false + tracer.push(`unsubscribing pubsub listener`); + await sentinel.unsubscribe('test') + tracer.push(`pubishing pubsub message`); + await sentinel.publish('test', 'hello world'); + await setTimeout(1000); + + tracer.push(`ensuring pubsub was unsubscribed via an assert`); + assert.equal(tester, false); + }); + + it('plain pubsub - pattern', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + await sentinel.connect(); + tracer.push(`connected`); + + let pubSubResolve; + const pubSubPromise = new Promise((res) => { + pubSubResolve = res; + }); + + let tester = false; + await sentinel.pSubscribe('test*', () => { + tracer.push(`got pubsub message`); + tester = true; + pubSubResolve(1); + }) + + tracer.push(`publishing pubsub message`); + await sentinel.publish('testy', 'hello world'); + tracer.push(`waiting on pubsub promise`); + await pubSubPromise; + tracer.push(`got pubsub promise`); + assert.equal(tester, true); + + // now unsubscribe + tester = false + tracer.push(`unsubscribing pubsub listener`); + await sentinel.pUnsubscribe('test*'); + tracer.push(`pubishing pubsub message`); + await sentinel.publish('testy', 'hello world'); + await setTimeout(1000); + + tracer.push(`ensuring pubsub was unsubscribed via an assert`); + assert.equal(tester, false); + }); + + // pubsub continues to work, even with a master change + it('pubsub - channel - with master change', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + tracer.push(`connected`); + + let pubSubResolve; + const pubSubPromise = new Promise((res) => { + pubSubResolve = res; + }) + + let tester = false; + await sentinel.subscribe('test', () => { + tracer.push(`got pubsub message`); + tester = true; + pubSubResolve(1); + }) + + let masterChangeResolve; + const masterChangePromise = new Promise((res) => { + masterChangeResolve = res; + }) + + const masterNode = sentinel.getMasterNode(); + tracer.push(`got masterPort as ${masterNode!.port}`); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push("got a master change event that is not the same as before"); + masterChangeResolve(event.node); + } + }); + + tracer.push("stopping master"); + await frame.stopNode(masterNode!.port.toString()); + tracer.push("stopped master and waiting on change promise"); + + const newMaster = await masterChangePromise as RedisNode; + tracer.push(`got master change port as ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); + + tracer.push(`publishing pubsub message`); + await sentinel.publish('test', 'hello world'); + tracer.push(`published pubsub message and waiting pn pubsub promise`); + await pubSubPromise; + tracer.push(`got pubsub promise`); + + assert.equal(tester, true); + + // now unsubscribe + tester = false + await sentinel.unsubscribe('test') + await sentinel.publish('test', 'hello world'); + await setTimeout(1000); + + assert.equal(tester, false); + }); + + it('pubsub - pattern - with master change', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + tracer.push(`connected`); + + let pubSubResolve; + const pubSubPromise = new Promise((res) => { + pubSubResolve = res; + }) + + let tester = false; + await sentinel.pSubscribe('test*', () => { + tracer.push(`got pubsub message`); + tester = true; + pubSubResolve(1); + }) + + let masterChangeResolve; + const masterChangePromise = new Promise((res) => { + masterChangeResolve = res; + }) + + const masterNode = sentinel.getMasterNode(); + tracer.push(`got masterPort as ${masterNode!.port}`); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push("got a master change event that is not the same as before"); + masterChangeResolve(event.node); + } + }); + + tracer.push("stopping master"); + await frame.stopNode(masterNode!.port.toString()); + tracer.push("stopped master and waiting on master change promise"); + + const newMaster = await masterChangePromise as RedisNode; + tracer.push(`got master change port as ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); + + tracer.push(`publishing pubsub message`); + await sentinel.publish('testy', 'hello world'); + tracer.push(`published pubsub message and waiting on pubsub promise`); + await pubSubPromise; + tracer.push(`got pubsub promise`); + assert.equal(tester, true); + + // now unsubscribe + tester = false + await sentinel.pUnsubscribe('test*'); + await sentinel.publish('testy', 'hello world'); + await setTimeout(1000); + + assert.equal(tester, false); + }); + + // if we stop a node, the comand should "retry" until we reconfigure topology and execute on new topology + it('command immeaditely after stopping master', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + + tracer.push("connected"); + + let masterChangeResolve; + const masterChangePromise = new Promise((res) => { + masterChangeResolve = res; + }) + + const masterNode = sentinel.getMasterNode(); + tracer.push(`original master port = ${masterNode!.port}`); + + let changeCount = 0; + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + changeCount++; + tracer.push(`got topology-change event we expected`); + masterChangeResolve(event.node); + } + }); + + tracer.push(`stopping masterNode`); + await frame.stopNode(masterNode!.port.toString()); + tracer.push(`stopped masterNode`); + assert.equal(await sentinel.set('x', 123), 'OK'); + tracer.push(`did the set operation`); + const presumamblyNewMaster = sentinel.getMasterNode(); + tracer.push(`new master node seems to be ${presumamblyNewMaster?.port} and waiting on master change promise`); + + const newMaster = await masterChangePromise as RedisNode; + tracer.push(`got new masternode event saying master is at ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); + + tracer.push(`doing the get`); + const val = await sentinel.get('x'); + tracer.push(`did the get and got ${val}`); + const newestMaster = sentinel.getMasterNode() + tracer.push(`after get, we see master as ${newestMaster?.port}`); + + switch (changeCount) { + case 1: + // if we only changed masters once, we should have the proper value + assert.equal(val, '123'); + break; + case 2: + // we changed masters twice quickly, so probably didn't replicate + // therefore, this is soewhat flakey, but the above is the common case + assert(val == '123' || val == null); + break; + default: + assert(false, "unexpected case"); + } + }); + + it('shutdown sentinel node', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + tracer.push("connected"); + + let sentinelChangeResolve; + const sentinelChangePromise = new Promise((res) => { + sentinelChangeResolve = res; + }) + + const sentinelNode = sentinel.getSentinelNode(); + tracer.push(`sentinelNode = ${sentinelNode?.port}`) + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "SENTINEL_CHANGE") { + tracer.push("got sentinel change event"); + sentinelChangeResolve(event.node); + } + }); + + tracer.push("Stopping sentinel node"); + await frame.stopSentinel(sentinelNode!.port.toString()); + tracer.push("Stopped sentinel node and waiting on sentinel change promise"); + const newSentinel = await sentinelChangePromise as RedisNode; + tracer.push("got sentinel change promise"); + assert.notEqual(sentinelNode!.port, newSentinel.port); + }); + + it('timer works, and updates sentinel list', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ scanInterval: 1000 }); + sentinel.setTracer(tracer); + await sentinel.connect(); + tracer.push("connected"); + + let sentinelChangeResolve; + const sentinelChangePromise = new Promise((res) => { + sentinelChangeResolve = res; + }) + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "SENTINE_LIST_CHANGE" && event.size == 4) { + tracer.push(`got sentinel list change event with right size`); + sentinelChangeResolve(event.size); + } + }); + + tracer.push(`adding sentinel`); + await frame.addSentinel(); + tracer.push(`added sentinel and waiting on sentinel change promise`); + const newSentinelSize = await sentinelChangePromise as number; + + assert.equal(newSentinelSize, 4); + }); + + it('stop replica, bring back replica', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); + sentinel.setTracer(tracer); + sentinel.on('error', err => { }); + await sentinel.connect(); + tracer.push("connected"); + + let sentinelRemoveResolve; + const sentinelRemovePromise = new Promise((res) => { + sentinelRemoveResolve = res; + }) + + const replicaPort = await frame.getRandonNonMasterNode(); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "REPLICA_REMOVE") { + if (event.node.port.toString() == replicaPort) { + tracer.push("got expected replica removed event"); + sentinelRemoveResolve(event.node); + } else { + tracer.push(`got replica removed event for a different node: ${event.node.port}`); + } + } + }); + + tracer.push(`replicaPort = ${replicaPort} and stopping it`); + await frame.stopNode(replicaPort); + tracer.push("stopped replica and waiting on sentinel removed promise"); + const stoppedNode = await sentinelRemovePromise as RedisNode; + tracer.push("got removed promise"); + assert.equal(stoppedNode.port, Number(replicaPort)); + + let sentinelRestartedResolve; + const sentinelRestartedPromise = new Promise((res) => { + sentinelRestartedResolve = res; + }) + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "REPLICA_ADD") { + tracer.push("got replica added event"); + sentinelRestartedResolve(event.node); + } + }); + + tracer.push("restarting replica"); + await frame.restartNode(replicaPort); + tracer.push("restarted replica and waiting on restart promise"); + const restartedNode = await sentinelRestartedPromise as RedisNode; + tracer.push("got restarted promise"); + assert.equal(restartedNode.port, Number(replicaPort)); + }) + + it('add a node / new replica', async function () { + this.timeout(30000); + + sentinel = frame.getSentinelClient({ scanInterval: 2000, replicaPoolSize: 1 }); + sentinel.setTracer(tracer); + // need to handle errors, as the spawning a new docker node can cause existing connections to time out + sentinel.on('error', err => { }); + await sentinel.connect(); + tracer.push("connected"); + + let nodeAddedResolve: (value: RedisNode) => void; + const nodeAddedPromise = new Promise((res) => { + nodeAddedResolve = res as (value: RedisNode) => void; + }); + + const portSet = new Set(); + for (const port of frame.getAllNodesPort()) { + portSet.add(port); + } + + // "on" and not "once" as due to connection timeouts, can happen multiple times, and want right one + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "REPLICA_ADD") { + if (!portSet.has(event.node.port)) { + tracer.push("got expected replica added event"); + nodeAddedResolve(event.node); + } + } + }); + + tracer.push("adding node"); + await frame.addNode(); + tracer.push("added node and waiting on added promise"); + await nodeAddedPromise; + }) + }) + + describe('Sentinel Factory', function () { + let master: RedisClientType | undefined; + let replica: RedisClientType | undefined; + + beforeEach(async function () { + this.timeout(0); + + await frame.getAllRunning(); + + await steadyState(frame); + longestTestDelta = 0; + }) + + afterEach(async function () { + if (this!.currentTest!.state === 'failed') { + console.log(`longest event loop blocked delta: ${longestDelta}`); + console.log(`longest event loop blocked in failing test: ${longestTestDelta}`); + console.log("trace:"); + for (const line of tracer) { + console.log(line); + } + const results = await Promise.all([ + frame.sentinelSentinels(), + frame.sentinelMaster(), + frame.sentinelReplicas() + ]) + console.log(`sentinel sentinels:\n${JSON.stringify(results[0], undefined, '\t')}`); + console.log(`sentinel master:\n${JSON.stringify(results[1], undefined, '\t')}`); + console.log(`sentinel replicas:\n${JSON.stringify(results[2], undefined, '\t')}`); + const { stdout, stderr } = await execAsync("docker ps -a"); + console.log(`docker stdout:\n${stdout}`); + console.log(`docker stderr:\n${stderr}`); + } + tracer.length = 0; + + if (master !== undefined) { + if (master.isOpen) { + master.destroy(); + } + master = undefined; + } + + if (replica !== undefined) { + if (replica.isOpen) { + replica.destroy(); + } + replica = undefined; + } + }) + + it('sentinel factory - master', async function () { + const sentinelPorts = frame.getAllSentinelsPort(); + const sentinels: Array = []; + for (const port of sentinelPorts) { + sentinels.push({ host: "localhost", port: port }); + } + + const factory = new RedisSentinelFactory({ name: frame.config.sentinelName, sentinelRootNodes: sentinels, sentinelClientOptions: {password: password}, nodeClientOptions: {password: password} }) + await factory.updateSentinelRootNodes(); + + master = await factory.getMasterClient(); + await master.connect(); + + assert.equal(await master.set("x", 1), 'OK'); + }) + + it('sentinel factory - replica', async function () { + const sentinelPorts = frame.getAllSentinelsPort(); + const sentinels: Array = []; + + for (const port of sentinelPorts) { + sentinels.push({ host: "localhost", port: port }); + } + + const factory = new RedisSentinelFactory({ name: frame.config.sentinelName, sentinelRootNodes: sentinels, sentinelClientOptions: {password: password}, nodeClientOptions: {password: password} }) + await factory.updateSentinelRootNodes(); + + const masterNode = await factory.getMasterNode(); + replica = await factory.getReplicaClient(); + const replicaSocketOptions = replica.options?.socket as unknown as RedisTcpSocketOptions | undefined; + assert.notEqual(masterNode.port, replicaSocketOptions?.port) + }) + + it('sentinel factory - bad node', async function () { + const factory = new RedisSentinelFactory({ name: frame.config.sentinelName, sentinelRootNodes: [{ host: "locahost", port: 1 }] }); + await assert.rejects(factory.updateSentinelRootNodes(), new Error("Couldn't connect to any sentinel node")); + }) + + it('sentinel factory - invalid db name', async function () { + this.timeout(15000); + + const sentinelPorts = frame.getAllSentinelsPort(); + const sentinels: Array = []; + + for (const port of sentinelPorts) { + sentinels.push({ host: "localhost", port: port }); + } + + const factory = new RedisSentinelFactory({ name: "invalid-name", sentinelRootNodes: sentinels, sentinelClientOptions: {password: password}, nodeClientOptions: {password: password} }) + await assert.rejects(factory.updateSentinelRootNodes(), new Error("ERR No such master with that name")); + }) + + it('sentinel factory - no available nodes', async function () { + this.timeout(15000); + + const sentinelPorts = frame.getAllSentinelsPort(); + const sentinels: Array = []; + + for (const port of sentinelPorts) { + sentinels.push({ host: "localhost", port: port }); + } + + const factory = new RedisSentinelFactory({ name: frame.config.sentinelName, sentinelRootNodes: sentinels, sentinelClientOptions: {password: password}, nodeClientOptions: {password: password} }) + + for (const node of frame.getAllNodesPort()) { + await frame.stopNode(node.toString()); + } + + await setTimeout(1000); + + await assert.rejects(factory.getMasterNode(), new Error("Master Node Not Enumerated")); + }) + }) + }) +}); diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts new file mode 100644 index 00000000000..b71514e9358 --- /dev/null +++ b/packages/client/lib/sentinel/index.ts @@ -0,0 +1,1487 @@ +import { EventEmitter } from 'node:events'; +import { CommandArguments, RedisArgument, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, TypeMapping } from '../RESP/types'; +import RedisClient, { RedisClientOptions, RedisClientType } from '../client'; +import { CommandOptions } from '../client/commands-queue'; +import { attachConfig } from '../commander'; +import COMMANDS from '../commands'; +import { ClientErrorEvent, NamespaceProxySentinel, NamespaceProxySentinelClient, ProxySentinel, ProxySentinelClient, RedisNode, RedisSentinelClientType, RedisSentinelEvent, RedisSentinelOptions, RedisSentinelType, SentinelCommander } from './types'; +import { clientSocketToNode, createCommand, createFunctionCommand, createModuleCommand, createNodeList, createScriptCommand, parseNode } from './utils'; +import { RedisMultiQueuedCommand } from '../multi-command'; +import RedisSentinelMultiCommand, { RedisSentinelMultiCommandType } from './multi-commands'; +import { PubSubListener } from '../client/pub-sub'; +import { PubSubProxy } from './pub-sub-proxy'; +import { setTimeout } from 'node:timers/promises'; +import RedisSentinelModule from './module' +import { RedisVariadicArgument } from '../commands/generic-transformers'; +import { WaitQueue } from './wait-queue'; +import { TcpNetConnectOpts } from 'node:net'; +import { RedisTcpSocketOptions } from '../client/socket'; + +interface ClientInfo { + id: number; +} + +export class RedisSentinelClient< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> { + #clientInfo: ClientInfo | undefined; + #internal: RedisSentinelInternal; + readonly _self: RedisSentinelClient; + + get isOpen() { + return this._self.#internal.isOpen; + } + + get isReady() { + return this._self.#internal.isReady; + } + + get commandOptions() { + return this._self.#commandOptions; + } + + #commandOptions?: CommandOptions; + + constructor( + internal: RedisSentinelInternal, + clientInfo: ClientInfo, + commandOptions?: CommandOptions + ) { + this._self = this; + this.#internal = internal; + this.#clientInfo = clientInfo; + this.#commandOptions = commandOptions; + } + + static factory< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + >(config?: SentinelCommander) { + const SentinelClient = attachConfig({ + BaseClass: RedisSentinelClient, + commands: COMMANDS, + createCommand: createCommand, + createModuleCommand: createModuleCommand, + createFunctionCommand: createFunctionCommand, + createScriptCommand: createScriptCommand, + config + }); + + SentinelClient.prototype.Multi = RedisSentinelMultiCommand.extend(config); + + return ( + internal: RedisSentinelInternal, + clientInfo: ClientInfo, + commandOptions?: CommandOptions + ) => { + // returning a "proxy" to prevent the namespaces._self to leak between "proxies" + return Object.create(new SentinelClient(internal, clientInfo, commandOptions)) as RedisSentinelClientType; + }; + } + + static create< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + >( + internal: RedisSentinelInternal, + clientInfo: ClientInfo, + commandOptions?: CommandOptions, + options?: RedisSentinelOptions + ) { + return RedisSentinelClient.factory(options)(internal, clientInfo, commandOptions); + } + + withCommandOptions< + OPTIONS extends CommandOptions, + TYPE_MAPPING extends TypeMapping + >(options: OPTIONS) { + const proxy = Object.create(this); + proxy._commandOptions = options; + return proxy as RedisSentinelClientType< + M, + F, + S, + RESP, + TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {} + >; + } + + private _commandOptionsProxy< + K extends keyof CommandOptions, + V extends CommandOptions[K] + >( + key: K, + value: V + ) { + const proxy = Object.create(this); + proxy._commandOptions = Object.create(this._self.#commandOptions ?? null); + proxy._commandOptions[key] = value; + return proxy as RedisSentinelClientType< + M, + F, + S, + RESP, + K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING + >; + } + + /** + * Override the `typeMapping` command option + */ + withTypeMapping(typeMapping: TYPE_MAPPING) { + return this._commandOptionsProxy('typeMapping', typeMapping); + } + + async _execute( + isReadonly: boolean | undefined, + fn: (client: RedisClient) => Promise + ): Promise { + if (this._self.#clientInfo === undefined) { + throw new Error("Attempted execution on released RedisSentinelClient lease"); + } + + return await this._self.#internal.execute(fn, this._self.#clientInfo); + } + + async sendCommand( + isReadonly: boolean | undefined, + args: CommandArguments, + options?: CommandOptions, + ): Promise { + return this._execute( + isReadonly, + client => client.sendCommand(args, options) + ); + } + + executeScript( + script: RedisScript, + isReadonly: boolean | undefined, + args: Array, + options?: CommandOptions + ) { + return this._execute( + isReadonly, + client => client.executeScript(script, args, options) + ); + } + + /** + * @internal + */ + async _executePipeline( + isReadonly: boolean | undefined, + commands: Array + ) { + return this._execute( + isReadonly, + client => client._executePipeline(commands) + ); + } + + /**f + * @internal + */ + async _executeMulti( + isReadonly: boolean | undefined, + commands: Array + ) { + return this._execute( + isReadonly, + client => client._executeMulti(commands) + ); + } + + MULTI(): RedisSentinelMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING> { + return new (this as any).Multi(this); + } + + multi = this.MULTI; + + WATCH(key: RedisVariadicArgument) { + if (this._self.#clientInfo === undefined) { + throw new Error("Attempted execution on released RedisSentinelClient lease"); + } + + return this._execute( + false, + client => client.watch(key) + ) + } + + watch = this.WATCH; + + UNWATCH() { + if (this._self.#clientInfo === undefined) { + throw new Error('Attempted execution on released RedisSentinelClient lease'); + } + + return this._execute( + false, + client => client.unwatch() + ) + } + + unwatch = this.UNWATCH; + + release() { + if (this._self.#clientInfo === undefined) { + throw new Error('RedisSentinelClient lease already released'); + } + + const result = this._self.#internal.releaseClientLease(this._self.#clientInfo); + this._self.#clientInfo = undefined; + return result; + } +} + +export default class RedisSentinel< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> extends EventEmitter { + readonly _self: RedisSentinel; + + #internal: RedisSentinelInternal; + #options: RedisSentinelOptions; + + get isOpen() { + return this._self.#internal.isOpen; + } + + get isReady() { + return this._self.#internal.isReady; + } + + get commandOptions() { + return this._self.#commandOptions; + } + + #commandOptions?: CommandOptions; + + #trace: (msg: string) => unknown = () => { }; + + #reservedClientInfo?: ClientInfo; + #masterClientCount = 0; + #masterClientInfo?: ClientInfo; + + constructor(options: RedisSentinelOptions) { + super(); + + this._self = this; + + this.#options = options; + + if (options?.commandOptions) { + this.#commandOptions = options.commandOptions; + } + + this.#internal = new RedisSentinelInternal(options); + this.#internal.on('error', err => this.emit('error', err)); + + /* pass through underling events */ + /* TODO: perhaps make this a struct and one vent, instead of multiple events */ + this.#internal.on('topology-change', (event: RedisSentinelEvent) => { + if (!this.emit('topology-change', event)) { + this._self.#trace(`RedisSentinel: re-emit for topology-change for ${event.type} event returned false`); + } + }); + } + + static factory< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + >(config?: SentinelCommander) { + const Sentinel = attachConfig({ + BaseClass: RedisSentinel, + commands: COMMANDS, + createCommand: createCommand, + createModuleCommand: createModuleCommand, + createFunctionCommand: createFunctionCommand, + createScriptCommand: createScriptCommand, + config + }); + + Sentinel.prototype.Multi = RedisSentinelMultiCommand.extend(config); + + return (options?: Omit>) => { + // returning a "proxy" to prevent the namespaces.self to leak between "proxies" + return Object.create(new Sentinel(options)) as RedisSentinelType; + }; + } + + static create< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + >(options?: RedisSentinelOptions) { + return RedisSentinel.factory(options)(options); + } + + withCommandOptions< + OPTIONS extends CommandOptions, + TYPE_MAPPING extends TypeMapping, + >(options: OPTIONS) { + const proxy = Object.create(this); + proxy._commandOptions = options; + return proxy as RedisSentinelType< + M, + F, + S, + RESP, + TYPE_MAPPING extends TypeMapping ? TYPE_MAPPING : {} + >; + } + + private _commandOptionsProxy< + K extends keyof CommandOptions, + V extends CommandOptions[K] + >( + key: K, + value: V + ) { + const proxy = Object.create(this._self); + proxy._commandOptions = Object.create(this._self.#commandOptions ?? null); + proxy._commandOptions[key] = value; + return proxy as RedisSentinelType< + M, + F, + S, + RESP, + K extends 'typeMapping' ? V extends TypeMapping ? V : {} : TYPE_MAPPING + >; + } + + /** + * Override the `typeMapping` command option + */ + withTypeMapping(typeMapping: TYPE_MAPPING) { + return this._commandOptionsProxy('typeMapping', typeMapping); + } + + async connect() { + await this._self.#internal.connect(); + + if (this._self.#options.reserveClient) { + this._self.#reservedClientInfo = await this._self.#internal.getClientLease(); + } + + return this as unknown as RedisSentinelType; + } + + async _execute( + isReadonly: boolean | undefined, + fn: (client: RedisClient) => Promise + ): Promise { + let clientInfo: ClientInfo | undefined; + if (!isReadonly || !this._self.#internal.useReplicas) { + if (this._self.#reservedClientInfo) { + clientInfo = this._self.#reservedClientInfo; + } else { + this._self.#masterClientInfo ??= await this._self.#internal.getClientLease(); + clientInfo = this._self.#masterClientInfo; + this._self.#masterClientCount++; + } + } + + try { + return await this._self.#internal.execute(fn, clientInfo); + } finally { + if ( + clientInfo !== undefined && + clientInfo === this._self.#masterClientInfo && + --this._self.#masterClientCount === 0 + ) { + const promise = this._self.#internal.releaseClientLease(clientInfo); + this._self.#masterClientInfo = undefined; + if (promise) await promise; + } + } + } + + async use(fn: (sentinelClient: RedisSentinelClientType) => Promise) { + const clientInfo = await this._self.#internal.getClientLease(); + + try { + return await fn( + RedisSentinelClient.create(this._self.#internal, clientInfo, this._self.#commandOptions, this._self.#options) + ); + } finally { + const promise = this._self.#internal.releaseClientLease(clientInfo); + if (promise) await promise; + } + } + + async sendCommand( + isReadonly: boolean | undefined, + args: CommandArguments, + options?: CommandOptions, + ): Promise { + return this._execute( + isReadonly, + client => client.sendCommand(args, options) + ); + } + + executeScript( + script: RedisScript, + isReadonly: boolean | undefined, + args: Array, + options?: CommandOptions + ) { + return this._execute( + isReadonly, + client => client.executeScript(script, args, options) + ); + } + + /** + * @internal + */ + async _executePipeline( + isReadonly: boolean | undefined, + commands: Array + ) { + return this._execute( + isReadonly, + client => client._executePipeline(commands) + ); + } + + /**f + * @internal + */ + async _executeMulti( + isReadonly: boolean | undefined, + commands: Array + ) { + return this._execute( + isReadonly, + client => client._executeMulti(commands) + ); + } + + MULTI(): RedisSentinelMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING> { + return new (this as any).Multi(this); + } + + multi = this.MULTI; + + async close() { + return this._self.#internal.close(); + } + + destroy() { + return this._self.#internal.destroy(); + } + + async SUBSCRIBE( + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + return this._self.#internal.subscribe(channels, listener, bufferMode); + } + + subscribe = this.SUBSCRIBE; + + async UNSUBSCRIBE( + channels?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ) { + return this._self.#internal.unsubscribe(channels, listener, bufferMode); + } + + unsubscribe = this.UNSUBSCRIBE; + + async PSUBSCRIBE( + patterns: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + return this._self.#internal.pSubscribe(patterns, listener, bufferMode); + } + + pSubscribe = this.PSUBSCRIBE; + + async PUNSUBSCRIBE( + patterns?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ) { + return this._self.#internal.pUnsubscribe(patterns, listener, bufferMode); + } + + pUnsubscribe = this.PUNSUBSCRIBE; + + async aquire(): Promise> { + const clientInfo = await this._self.#internal.getClientLease(); + return RedisSentinelClient.create(this._self.#internal, clientInfo, this._self.#commandOptions, this._self.#options); + } + + getSentinelNode(): RedisNode | undefined { + return this._self.#internal.getSentinelNode(); + } + + getMasterNode(): RedisNode | undefined { + return this._self.#internal.getMasterNode(); + } + + getReplicaNodes(): Map { + return this._self.#internal.getReplicaNodes(); + } + + setTracer(tracer?: Array) { + if (tracer) { + this._self.#trace = (msg: string) => { tracer.push(msg) }; + } else { + this._self.#trace = () => { }; + } + + this._self.#internal.setTracer(tracer); + } +} + +class RedisSentinelInternal< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> extends EventEmitter { + #isOpen = false; + + get isOpen() { + return this.#isOpen; + } + + #isReady = false; + + get isReady() { + return this.#isReady; + } + + readonly #name: string; + readonly #nodeClientOptions: RedisClientOptions; + readonly #sentinelClientOptions: RedisClientOptions; + readonly #scanInterval: number; + readonly #passthroughClientErrorEvents: boolean; + + #anotherReset = false; + + #configEpoch: number = 0; + + #sentinelRootNodes: Array; + #sentinelClient?: RedisClientType; + + #masterClients: Array> = []; + #masterClientQueue: WaitQueue; + readonly #masterPoolSize: number; + + #replicaClients: Array> = []; + #replicaClientsIdx: number = 0; + readonly #replicaPoolSize: number; + + get useReplicas() { + return this.#replicaPoolSize > 0; + } + + #connectPromise?: Promise; + #maxCommandRediscovers: number; + readonly #pubSubProxy: PubSubProxy; + + #scanTimer?: NodeJS.Timeout + + #destroy = false; + + #trace: (msg: string) => unknown = () => { }; + + constructor(options: RedisSentinelOptions) { + super(); + + this.#name = options.name; + + this.#sentinelRootNodes = Array.from(options.sentinelRootNodes); + this.#maxCommandRediscovers = options.maxCommandRediscovers ?? 16; + this.#masterPoolSize = options.masterPoolSize ?? 1; + this.#replicaPoolSize = options.replicaPoolSize ?? 0; + this.#scanInterval = options.scanInterval ?? 0; + this.#passthroughClientErrorEvents = options.passthroughClientErrorEvents ?? false; + + this.#nodeClientOptions = options.nodeClientOptions ? Object.assign({} as RedisClientOptions, options.nodeClientOptions) : {}; + if (this.#nodeClientOptions.url !== undefined) { + throw new Error("invalid nodeClientOptions for Sentinel"); + } + + this.#sentinelClientOptions = options.sentinelClientOptions ? Object.assign({} as RedisClientOptions, options.sentinelClientOptions) : {}; + this.#sentinelClientOptions.modules = RedisSentinelModule; + + if (this.#sentinelClientOptions.url !== undefined) { + throw new Error("invalid sentinelClientOptions for Sentinel"); + } + + this.#masterClientQueue = new WaitQueue(); + for (let i = 0; i < this.#masterPoolSize; i++) { + this.#masterClientQueue.push(i); + } + + /* persistent object for life of sentinel object */ + this.#pubSubProxy = new PubSubProxy( + this.#nodeClientOptions, + err => this.emit('error', err) + ); + } + + #createClient(node: RedisNode, clientOptions: RedisClientOptions, reconnectStrategy?: undefined | false) { + return RedisClient.create({ + ...clientOptions, + socket: { + ...clientOptions.socket, + host: node.host, + port: node.port, + reconnectStrategy + } + }); + } + + getClientLease(): ClientInfo | Promise { + const id = this.#masterClientQueue.shift(); + if (id !== undefined) { + return { id }; + } + + return this.#masterClientQueue.wait().then(id => ({ id })); + } + + releaseClientLease(clientInfo: ClientInfo) { + const client = this.#masterClients[clientInfo.id]; + // client can be undefined if releasing in middle of a reconfigure + if (client !== undefined) { + const dirtyPromise = client.resetIfDirty(); + if (dirtyPromise) { + return dirtyPromise + .then(() => this.#masterClientQueue.push(clientInfo.id)); + } + } + + this.#masterClientQueue.push(clientInfo.id); + } + + async connect() { + if (this.#isOpen) { + throw new Error("already attempting to open") + } + + try { + this.#isOpen = true; + + this.#connectPromise = this.#connect(); + await this.#connectPromise; + this.#isReady = true; + } finally { + this.#connectPromise = undefined; + if (this.#scanInterval > 0) { + this.#scanTimer = setInterval(this.#reset.bind(this), this.#scanInterval); + } + } + } + + async #connect() { + let count = 0; + while (true) { + this.#trace("starting connect loop"); + + if (this.#destroy) { + this.#trace("in #connect and want to destroy") + return; + } + try { + this.#anotherReset = false; + await this.transform(this.analyze(await this.observe())); + if (this.#anotherReset) { + this.#trace("#connect: anotherReset is true, so continuing"); + continue; + } + + this.#trace("#connect: returning"); + return; + } catch (e: any) { + this.#trace(`#connect: exception ${e.message}`); + if (!this.#isReady && count > this.#maxCommandRediscovers) { + throw e; + } + + if (e.message !== 'no valid master node') { + console.log(e); + } + await setTimeout(1000); + } finally { + this.#trace("finished connect"); + } + } + } + + async execute( + fn: (client: RedisClientType) => Promise, + clientInfo?: ClientInfo + ): Promise { + let iter = 0; + + while (true) { + if (this.#connectPromise !== undefined) { + await this.#connectPromise; + } + + const client = this.#getClient(clientInfo); + + if (!client.isReady) { + await this.#reset(); + continue; + } + const sockOpts = client.options?.socket as TcpNetConnectOpts | undefined; + this.#trace("attemping to send command to " + sockOpts?.host + ":" + sockOpts?.port) + + try { + /* + // force testing of READONLY errors + if (clientInfo !== undefined) { + if (Math.floor(Math.random() * 10) < 1) { + console.log("throwing READONLY error"); + throw new Error("READONLY You can't write against a read only replica."); + } + } + */ + return await fn(client); + } catch (err) { + if (++iter > this.#maxCommandRediscovers || !(err instanceof Error)) { + throw err; + } + + /* + rediscover and retry if doing a command against a "master" + a) READONLY error (topology has changed) but we haven't been notified yet via pubsub + b) client is "not ready" (disconnected), which means topology might have changed, but sentinel might not see it yet + */ + if (clientInfo !== undefined && (err.message.startsWith('READONLY') || !client.isReady)) { + await this.#reset(); + continue; + } + + throw err; + } + } + } + + async #createPubSub(client: RedisClientType) { + /* Whenever sentinels or slaves get added, or when slave configuration changes, reconfigure */ + await client.pSubscribe(['switch-master', '[-+]sdown', '+slave', '+sentinel', '[-+]odown', '+slave-reconf-done'], (message, channel) => { + this.#handlePubSubControlChannel(channel, message); + }, true); + + return client; + } + + async #handlePubSubControlChannel(channel: Buffer, message: Buffer) { + this.#trace("pubsub control channel message on " + channel); + this.#reset(); + } + + // if clientInfo is defined, it corresponds to a master client in the #masterClients array, otherwise loop around replicaClients + #getClient(clientInfo?: ClientInfo): RedisClientType { + if (clientInfo !== undefined) { + return this.#masterClients[clientInfo.id]; + } + + if (this.#replicaClientsIdx >= this.#replicaClients.length) { + this.#replicaClientsIdx = 0; + } + + if (this.#replicaClients.length == 0) { + throw new Error("no replicas available for read"); + } + + return this.#replicaClients[this.#replicaClientsIdx++]; + } + + async #reset() { + /* closing / don't reset */ + if (this.#isReady == false || this.#destroy == true) { + return; + } + + // already in #connect() + if (this.#connectPromise !== undefined) { + this.#anotherReset = true; + return await this.#connectPromise; + } + + try { + this.#connectPromise = this.#connect(); + return await this.#connectPromise; + } finally { + this.#trace("finished reconfgure"); + this.#connectPromise = undefined; + } + } + + async close() { + this.#destroy = true; + + if (this.#connectPromise != undefined) { + await this.#connectPromise; + } + + this.#isReady = false; + + if (this.#scanTimer) { + clearInterval(this.#scanTimer); + this.#scanTimer = undefined; + } + + const promises = []; + + if (this.#sentinelClient !== undefined) { + if (this.#sentinelClient.isOpen) { + promises.push(this.#sentinelClient.close()); + } + this.#sentinelClient = undefined; + } + + for (const client of this.#masterClients) { + if (client.isOpen) { + promises.push(client.close()); + } + } + + this.#masterClients = []; + + for (const client of this.#replicaClients) { + if (client.isOpen) { + promises.push(client.close()); + } + } + + this.#replicaClients = []; + + await Promise.all(promises); + + this.#pubSubProxy.destroy(); + + this.#isOpen = false; + } + + // destroy has to be async because its stopping others async events, timers and the like + // and shouldn't return until its finished. + async destroy() { + this.#destroy = true; + + if (this.#connectPromise != undefined) { + await this.#connectPromise; + } + + this.#isReady = false; + + if (this.#scanTimer) { + clearInterval(this.#scanTimer); + this.#scanTimer = undefined; + } + + if (this.#sentinelClient !== undefined) { + if (this.#sentinelClient.isOpen) { + this.#sentinelClient.destroy(); + } + this.#sentinelClient = undefined; + } + + for (const client of this.#masterClients) { + if (client.isOpen) { + client.destroy(); + } + } + this.#masterClients = []; + + for (const client of this.#replicaClients) { + if (client.isOpen) { + client.destroy(); + } + } + this.#replicaClients = []; + + this.#pubSubProxy.destroy(); + + this.#isOpen = false + this.#destroy = false; + } + + async subscribe( + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + return this.#pubSubProxy.subscribe(channels, listener, bufferMode); + } + + async unsubscribe( + channels?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ) { + return this.#pubSubProxy.unsubscribe(channels, listener, bufferMode); + } + + async pSubscribe( + patterns: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + return this.#pubSubProxy.pSubscribe(patterns, listener, bufferMode); + } + + async pUnsubscribe( + patterns?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ) { + return this.#pubSubProxy.pUnsubscribe(patterns, listener, bufferMode); + } + + // observe/analyze/transform remediation functions + async observe() { + for (const node of this.#sentinelRootNodes) { + let client: RedisClientType | undefined; + try { + this.#trace(`observe: trying to connect to sentinel: ${node.host}:${node.port}`) + client = this.#createClient(node, this.#sentinelClientOptions, false) as unknown as RedisClientType; + client.on('error', (err) => this.emit('error', `obseve client error: ${err}`)); + await client.connect(); + this.#trace(`observe: connected to sentinel`) + + const [sentinelData, masterData, replicaData] = await Promise.all([ + client.sentinel.sentinelSentinels(this.#name), + client.sentinel.sentinelMaster(this.#name), + client.sentinel.sentinelReplicas(this.#name) + ]); + + this.#trace("observe: got all sentinel data"); + + const ret = { + sentinelConnected: node, + sentinelData: sentinelData, + masterData: masterData, + replicaData: replicaData, + currentMaster: this.getMasterNode(), + currentReplicas: this.getReplicaNodes(), + currentSentinel: this.getSentinelNode(), + replicaPoolSize: this.#replicaPoolSize, + useReplicas: this.useReplicas + } + + return ret; + } catch (err) { + this.#trace(`observe: error ${err}`); + this.emit('error', err); + } finally { + if (client !== undefined && client.isOpen) { + this.#trace(`observe: destroying sentinel client`); + client.destroy(); + } + } + } + + this.#trace(`observe: none of the sentinels are available`); + throw new Error('None of the sentinels are available'); + } + + analyze(observed: Awaited["observe"]>>) { + let master = parseNode(observed.masterData); + if (master === undefined) { + this.#trace(`analyze: no valid master node because ${observed.masterData.flags}`); + throw new Error("no valid master node"); + } + + if (master.host === observed.currentMaster?.host && master.port === observed.currentMaster?.port) { + this.#trace(`analyze: master node hasn't changed from ${observed.currentMaster?.host}:${observed.currentMaster?.port}`); + master = undefined; + } else { + this.#trace(`analyze: master node has changed to ${master.host}:${master.port} from ${observed.currentMaster?.host}:${observed.currentMaster?.port}`); + } + + let sentinel: RedisNode | undefined = observed.sentinelConnected; + if (sentinel.host === observed.currentSentinel?.host && sentinel.port === observed.currentSentinel.port) { + this.#trace(`analyze: sentinel node hasn't changed`); + sentinel = undefined; + } else { + this.#trace(`analyze: sentinel node has changed to ${sentinel.host}:${sentinel.port}`); + } + + const replicasToClose: Array = []; + const replicasToOpen = new Map(); + + const desiredSet = new Set(); + const seen = new Set(); + + if (observed.useReplicas) { + const replicaList = createNodeList(observed.replicaData) + + for (const node of replicaList) { + desiredSet.add(JSON.stringify(node)); + } + + for (const [node, value] of observed.currentReplicas) { + if (!desiredSet.has(JSON.stringify(node))) { + replicasToClose.push(node); + this.#trace(`analyze: adding ${node.host}:${node.port} to replicsToClose`); + } else { + seen.add(JSON.stringify(node)); + if (value != observed.replicaPoolSize) { + replicasToOpen.set(node, observed.replicaPoolSize - value); + this.#trace(`analyze: adding ${node.host}:${node.port} to replicsToOpen`); + } + } + } + + for (const node of replicaList) { + if (!seen.has(JSON.stringify(node))) { + replicasToOpen.set(node, observed.replicaPoolSize); + this.#trace(`analyze: adding ${node.host}:${node.port} to replicsToOpen`); + } + } + } + + const ret = { + sentinelList: [observed.sentinelConnected].concat(createNodeList(observed.sentinelData)), + epoch: Number(observed.masterData['config-epoch']), + + sentinelToOpen: sentinel, + masterToOpen: master, + replicasToClose: replicasToClose, + replicasToOpen: replicasToOpen, + }; + + return ret; + } + + async transform(analyzed: ReturnType["analyze"]>) { + this.#trace("transform: enter"); + + let promises: Array> = []; + + if (analyzed.sentinelToOpen) { + this.#trace(`transform: opening a new sentinel`); + if (this.#sentinelClient !== undefined && this.#sentinelClient.isOpen) { + this.#trace(`transform: destroying old sentinel as open`); + this.#sentinelClient.destroy() + this.#sentinelClient = undefined; + } else { + this.#trace(`transform: not destroying old sentinel as not open`); + } + + this.#trace(`transform: creating new sentinel to ${analyzed.sentinelToOpen.host}:${analyzed.sentinelToOpen.port}`); + const node = analyzed.sentinelToOpen; + const client = this.#createClient(analyzed.sentinelToOpen, this.#sentinelClientOptions, false); + client.on('error', (err: Error) => { + if (this.#passthroughClientErrorEvents) { + this.emit('error', new Error(`Sentinel Client (${node.host}:${node.port}): ${err.message}`, { cause: err })); + } + const event: ClientErrorEvent = { + type: 'SENTINEL', + node: clientSocketToNode(client.options!.socket!), + error: err + }; + this.emit('client-error', event); + this.#reset(); + }); + this.#sentinelClient = client; + + this.#trace(`transform: adding sentinel client connect() to promise list`); + const promise = this.#sentinelClient.connect().then((client) => { return this.#createPubSub(client) }); + promises.push(promise); + + this.#trace(`created sentinel client to ${analyzed.sentinelToOpen.host}:${analyzed.sentinelToOpen.port}`); + const event: RedisSentinelEvent = { + type: "SENTINEL_CHANGE", + node: analyzed.sentinelToOpen + } + this.#trace(`transform: emiting topology-change event for sentinel_change`); + if (!this.emit('topology-change', event)) { + this.#trace(`transform: emit for topology-change for sentinel_change returned false`); + } + } + + if (analyzed.masterToOpen) { + this.#trace(`transform: opening a new master`); + const masterPromises = []; + const masterWatches: Array = []; + + this.#trace(`transform: destroying old masters if open`); + for (const client of this.#masterClients) { + masterWatches.push(client.isWatching); + + if (client.isOpen) { + client.destroy() + } + } + + this.#masterClients = []; + + this.#trace(`transform: creating all master clients and adding connect promises`); + for (let i = 0; i < this.#masterPoolSize; i++) { + const node = analyzed.masterToOpen; + const client = this.#createClient(analyzed.masterToOpen, this.#nodeClientOptions); + client.on('error', (err: Error) => { + if (this.#passthroughClientErrorEvents) { + this.emit('error', new Error(`Master Client (${node.host}:${node.port}): ${err.message}`, { cause: err })); + } + const event: ClientErrorEvent = { + type: "MASTER", + node: clientSocketToNode(client.options!.socket!), + error: err + }; + this.emit('client-error', event); + }); + + if (masterWatches[i]) { + client.setDirtyWatch("sentinel config changed in middle of a WATCH Transaction"); + } + this.#masterClients.push(client); + masterPromises.push(client.connect()); + + this.#trace(`created master client to ${analyzed.masterToOpen.host}:${analyzed.masterToOpen.port}`); + } + + this.#trace(`transform: adding promise to change #pubSubProxy node`); + masterPromises.push(this.#pubSubProxy.changeNode(analyzed.masterToOpen)); + promises.push(...masterPromises); + const event: RedisSentinelEvent = { + type: "MASTER_CHANGE", + node: analyzed.masterToOpen + } + this.#trace(`transform: emiting topology-change event for master_change`); + if (!this.emit('topology-change', event)) { + this.#trace(`transform: emit for topology-change for master_change returned false`); + } + this.#configEpoch++; + } + + const replicaCloseSet = new Set(); + for (const node of analyzed.replicasToClose) { + const str = JSON.stringify(node); + replicaCloseSet.add(str); + } + + const newClientList: Array> = []; + const removedSet = new Set(); + + for (const replica of this.#replicaClients) { + const node = clientSocketToNode(replica.options!.socket!); + const str = JSON.stringify(node); + + if (replicaCloseSet.has(str) || !replica.isOpen) { + if (replica.isOpen) { + const sockOpts = replica.options?.socket as TcpNetConnectOpts | undefined; + this.#trace(`destroying replica client to ${sockOpts?.host}:${sockOpts?.port}`); + replica.destroy() + } + if (!removedSet.has(str)) { + const event: RedisSentinelEvent = { + type: "REPLICA_REMOVE", + node: node + } + this.emit('topology-change', event); + removedSet.add(str); + } + } else { + newClientList.push(replica); + } + } + this.#replicaClients = newClientList; + + if (analyzed.replicasToOpen.size != 0) { + for (const [node, size] of analyzed.replicasToOpen) { + for (let i = 0; i < size; i++) { + const client = this.#createClient(node, this.#nodeClientOptions); + client.on('error', (err: Error) => { + if (this.#passthroughClientErrorEvents) { + this.emit('error', new Error(`Replica Client (${node.host}:${node.port}): ${err.message}`, { cause: err })); + } + const event: ClientErrorEvent = { + type: "REPLICA", + node: clientSocketToNode(client.options!.socket!), + error: err + }; + this.emit('client-error', event); + }); + + this.#replicaClients.push(client); + promises.push(client.connect()); + + this.#trace(`created replica client to ${node.host}:${node.port}`); + } + const event: RedisSentinelEvent = { + type: "REPLICA_ADD", + node: node + } + this.emit('topology-change', event); + } + } + + if (analyzed.sentinelList.length != this.#sentinelRootNodes.length) { + this.#sentinelRootNodes = analyzed.sentinelList; + const event: RedisSentinelEvent = { + type: "SENTINE_LIST_CHANGE", + size: analyzed.sentinelList.length + } + this.emit('topology-change', event); + } + + await Promise.all(promises); + this.#trace("transform: exit"); + } + + // introspection functions + getMasterNode(): RedisNode | undefined { + if (this.#masterClients.length == 0) { + return undefined; + } + + for (const master of this.#masterClients) { + if (master.isReady) { + return clientSocketToNode(master.options!.socket!); + } + } + + return undefined; + } + + getSentinelNode(): RedisNode | undefined { + if (this.#sentinelClient === undefined) { + return undefined; + } + + return clientSocketToNode(this.#sentinelClient.options!.socket!); + } + + getReplicaNodes(): Map { + const ret = new Map(); + const initialMap = new Map(); + + for (const replica of this.#replicaClients) { + const node = clientSocketToNode(replica.options!.socket!); + const hash = JSON.stringify(node); + + if (replica.isReady) { + initialMap.set(hash, (initialMap.get(hash) ?? 0) + 1); + } else { + if (!initialMap.has(hash)) { + initialMap.set(hash, 0); + } + } + } + + for (const [key, value] of initialMap) { + ret.set(JSON.parse(key) as RedisNode, value); + } + + return ret; + } + + setTracer(tracer?: Array) { + if (tracer) { + this.#trace = (msg: string) => { tracer.push(msg) }; + } else { + // empty function is faster than testing if something is defined or not + this.#trace = () => { }; + } + } +} + +export class RedisSentinelFactory extends EventEmitter { + options: RedisSentinelOptions; + #sentinelRootNodes: Array; + #replicaIdx: number = -1; + + constructor(options: RedisSentinelOptions) { + super(); + + this.options = options; + this.#sentinelRootNodes = options.sentinelRootNodes; + } + + async updateSentinelRootNodes() { + for (const node of this.#sentinelRootNodes) { + const client = RedisClient.create({ + ...this.options.sentinelClientOptions, + socket: { + ...this.options.sentinelClientOptions?.socket, + host: node.host, + port: node.port, + reconnectStrategy: false + }, + modules: RedisSentinelModule + }).on('error', (err) => this.emit(`updateSentinelRootNodes: ${err}`)); + try { + await client.connect(); + } catch { + if (client.isOpen) { + client.destroy(); + } + continue; + } + + try { + const sentinelData = await client.sentinel.sentinelSentinels(this.options.name); + this.#sentinelRootNodes = [node].concat(createNodeList(sentinelData)); + return; + } finally { + client.destroy(); + } + } + + throw new Error("Couldn't connect to any sentinel node"); + } + + async getMasterNode() { + let connected = false; + + for (const node of this.#sentinelRootNodes) { + const client = RedisClient.create({ + ...this.options.sentinelClientOptions, + socket: { + ...this.options.sentinelClientOptions?.socket, + host: node.host, + port: node.port, + reconnectStrategy: false + }, + modules: RedisSentinelModule + }).on('error', err => this.emit(`getMasterNode: ${err}`)); + + try { + await client.connect(); + } catch { + if (client.isOpen) { + client.destroy(); + } + continue; + } + + connected = true; + + try { + const masterData = await client.sentinel.sentinelMaster(this.options.name); + + let master = parseNode(masterData); + if (master === undefined) { + continue; + } + + return master; + } finally { + client.destroy(); + } + } + + if (connected) { + throw new Error("Master Node Not Enumerated"); + } + + throw new Error("couldn't connect to any sentinels"); + } + + async getMasterClient() { + const master = await this.getMasterNode(); + return RedisClient.create({ + ...this.options.nodeClientOptions, + socket: { + ...this.options.nodeClientOptions?.socket, + host: master.host, + port: master.port + } + }); + } + + async getReplicaNodes() { + let connected = false; + + for (const node of this.#sentinelRootNodes) { + const client = RedisClient.create({ + ...this.options.sentinelClientOptions, + socket: { + ...this.options.sentinelClientOptions?.socket, + host: node.host, + port: node.port, + reconnectStrategy: false + }, + modules: RedisSentinelModule + }).on('error', err => this.emit(`getReplicaNodes: ${err}`)); + + try { + await client.connect(); + } catch { + if (client.isOpen) { + client.destroy(); + } + continue; + } + + connected = true; + + try { + const replicaData = await client.sentinel.sentinelReplicas(this.options.name); + + const replicas = createNodeList(replicaData); + if (replicas.length == 0) { + continue; + } + + return replicas; + } finally { + client.destroy(); + } + } + + if (connected) { + throw new Error("No Replicas Nodes Enumerated"); + } + + throw new Error("couldn't connect to any sentinels"); + } + + async getReplicaClient() { + const replicas = await this.getReplicaNodes(); + if (replicas.length == 0) { + throw new Error("no available replicas"); + } + + this.#replicaIdx++; + if (this.#replicaIdx >= replicas.length) { + this.#replicaIdx = 0; + } + + return RedisClient.create({ + ...this.options.nodeClientOptions, + socket: { + ...this.options.nodeClientOptions?.socket, + host: replicas[this.#replicaIdx].host, + port: replicas[this.#replicaIdx].port + } + }); + } +} diff --git a/packages/client/lib/sentinel/module.ts b/packages/client/lib/sentinel/module.ts new file mode 100644 index 00000000000..e6e98e72f6d --- /dev/null +++ b/packages/client/lib/sentinel/module.ts @@ -0,0 +1,7 @@ + +import { RedisModules } from '../RESP/types'; +import sentinel from './commands'; + +export default { + sentinel +} as const satisfies RedisModules; diff --git a/packages/client/lib/sentinel/multi-commands.ts b/packages/client/lib/sentinel/multi-commands.ts new file mode 100644 index 00000000000..bf616370bf3 --- /dev/null +++ b/packages/client/lib/sentinel/multi-commands.ts @@ -0,0 +1,219 @@ +import COMMANDS from '../commands'; +import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType } from '../multi-command'; +import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping } from '../RESP/types'; +import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander'; +import { RedisSentinelType } from './types'; + +type CommandSignature< + REPLIES extends Array, + C extends Command, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = (...args: Parameters) => RedisSentinelMultiCommandType< + [...REPLIES, ReplyWithTypeMapping, TYPE_MAPPING>], + M, + F, + S, + RESP, + TYPE_MAPPING +>; + +type WithCommands< + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof typeof COMMANDS]: CommandSignature; +}; + +type WithModules< + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof M]: { + [C in keyof M[P]]: CommandSignature; + }; +}; + +type WithFunctions< + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [L in keyof F]: { + [C in keyof F[L]]: CommandSignature; + }; +}; + +type WithScripts< + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof S]: CommandSignature; +}; + +export type RedisSentinelMultiCommandType< + REPLIES extends Array, + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = ( + RedisSentinelMultiCommand & + WithCommands & + WithModules & + WithFunctions & + WithScripts +); + +export default class RedisSentinelMultiCommand { + private static _createCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return function (this: RedisSentinelMultiCommand, ...args: Array) { + const redisArgs = command.transformArguments(...args); + return this.addCommand( + command.IS_READ_ONLY, + redisArgs, + transformReply + ); + }; + } + + private static _createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return function (this: { _self: RedisSentinelMultiCommand }, ...args: Array) { + const redisArgs = command.transformArguments(...args); + return this._self.addCommand( + command.IS_READ_ONLY, + redisArgs, + transformReply + ); + }; + } + + private static _createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn), + transformReply = getTransformReply(fn, resp); + return function (this: { _self: RedisSentinelMultiCommand }, ...args: Array) { + const fnArgs = fn.transformArguments(...args); + const redisArgs: CommandArguments = prefix.concat(fnArgs); + redisArgs.preserve = fnArgs.preserve; + return this._self.addCommand( + fn.IS_READ_ONLY, + redisArgs, + transformReply + ); + }; + } + + private static _createScriptCommand(script: RedisScript, resp: RespVersions) { + const transformReply = getTransformReply(script, resp); + return function (this: RedisSentinelMultiCommand, ...args: Array) { + const scriptArgs = script.transformArguments(...args); + this._setState( + script.IS_READ_ONLY + ); + this._multi.addScript( + script, + scriptArgs, + transformReply + ); + return this; + }; + } + + static extend< + M extends RedisModules = Record, + F extends RedisFunctions = Record, + S extends RedisScripts = Record, + RESP extends RespVersions = 2 + >(config?: CommanderConfig) { + return attachConfig({ + BaseClass: RedisSentinelMultiCommand, + commands: COMMANDS, + createCommand: RedisSentinelMultiCommand._createCommand, + createModuleCommand: RedisSentinelMultiCommand._createModuleCommand, + createFunctionCommand: RedisSentinelMultiCommand._createFunctionCommand, + createScriptCommand: RedisSentinelMultiCommand._createScriptCommand, + config + }); + } + + private readonly _multi = new RedisMultiCommand(); + private readonly _sentinel: RedisSentinelType + private _isReadonly: boolean | undefined = true; + private readonly _typeMapping?: TypeMapping; + + constructor(sentinel: RedisSentinelType, typeMapping: TypeMapping) { + this._sentinel = sentinel; + this._typeMapping = typeMapping; + } + + private _setState( + isReadonly: boolean | undefined, + ) { + this._isReadonly &&= isReadonly; + } + + addCommand( + isReadonly: boolean | undefined, + args: CommandArguments, + transformReply?: TransformReply + ) { + this._setState(isReadonly); + this._multi.addCommand(args, transformReply); + return this; + } + + async exec(execAsPipeline = false) { + if (execAsPipeline) return this.execAsPipeline(); + + return this._multi.transformReplies( + await this._sentinel._executeMulti( + this._isReadonly, + this._multi.queue + ), + this._typeMapping + ) as MultiReplyType; + } + + EXEC = this.exec; + + execTyped(execAsPipeline = false) { + return this.exec(execAsPipeline); + } + + async execAsPipeline() { + if (this._multi.queue.length === 0) return [] as MultiReplyType; + + return this._multi.transformReplies( + await this._sentinel._executePipeline( + this._isReadonly, + this._multi.queue + ), + this._typeMapping + ) as MultiReplyType; + } + + execAsPipelineTyped() { + return this.execAsPipeline(); + } +} diff --git a/packages/client/lib/sentinel/pub-sub-proxy.ts b/packages/client/lib/sentinel/pub-sub-proxy.ts new file mode 100644 index 00000000000..68a6c3b58e6 --- /dev/null +++ b/packages/client/lib/sentinel/pub-sub-proxy.ts @@ -0,0 +1,209 @@ +import EventEmitter from 'node:events'; +import { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; +import { RedisClientOptions } from '../client'; +import { PUBSUB_TYPE, PubSubListener, PubSubTypeListeners } from '../client/pub-sub'; +import { RedisNode } from './types'; +import RedisClient from '../client'; + +type Client = RedisClient< + RedisModules, + RedisFunctions, + RedisScripts, + RespVersions, + TypeMapping +>; + +type Subscriptions = Record< + PUBSUB_TYPE['CHANNELS'] | PUBSUB_TYPE['PATTERNS'], + PubSubTypeListeners +>; + +type PubSubState = { + client: Client; + connectPromise: Promise | undefined; +}; + +type OnError = (err: unknown) => unknown; + +export class PubSubProxy extends EventEmitter { + #clientOptions; + #onError; + + #node?: RedisNode; + #state?: PubSubState; + #subscriptions?: Subscriptions; + + constructor(clientOptions: RedisClientOptions, onError: OnError) { + super(); + + this.#clientOptions = clientOptions; + this.#onError = onError; + } + + #createClient() { + if (this.#node === undefined) { + throw new Error("pubSubProxy: didn't define node to do pubsub against"); + } + + return new RedisClient({ + ...this.#clientOptions, + socket: { + ...this.#clientOptions.socket, + host: this.#node.host, + port: this.#node.port + } + }); + } + + async #initiatePubSubClient(withSubscriptions = false) { + const client = this.#createClient() + .on('error', this.#onError); + + const connectPromise = client.connect() + .then(async client => { + if (this.#state?.client !== client) { + // if pubsub was deactivated while connecting (`this.#pubSubClient === undefined`) + // or if the node changed (`this.#pubSubClient.client !== client`) + client.destroy(); + return this.#state?.connectPromise; + } + + if (withSubscriptions && this.#subscriptions) { + await Promise.all([ + client.extendPubSubListeners(PUBSUB_TYPE.CHANNELS, this.#subscriptions[PUBSUB_TYPE.CHANNELS]), + client.extendPubSubListeners(PUBSUB_TYPE.PATTERNS, this.#subscriptions[PUBSUB_TYPE.PATTERNS]) + ]); + } + + if (this.#state.client !== client) { + // if the node changed (`this.#pubSubClient.client !== client`) + client.destroy(); + return this.#state?.connectPromise; + } + + this.#state!.connectPromise = undefined; + return client; + }) + .catch(err => { + this.#state = undefined; + throw err; + }); + + this.#state = { + client, + connectPromise + }; + + return connectPromise; + } + + #getPubSubClient() { + if (!this.#state) return this.#initiatePubSubClient(); + + return ( + this.#state.connectPromise ?? + this.#state.client + ); + } + + async changeNode(node: RedisNode) { + this.#node = node; + + if (!this.#state) return; + + // if `connectPromise` is undefined, `this.#subscriptions` is already set + // and `this.#state.client` might not have the listeners set yet + if (this.#state.connectPromise === undefined) { + this.#subscriptions = { + [PUBSUB_TYPE.CHANNELS]: this.#state.client.getPubSubListeners(PUBSUB_TYPE.CHANNELS), + [PUBSUB_TYPE.PATTERNS]: this.#state.client.getPubSubListeners(PUBSUB_TYPE.PATTERNS) + }; + + this.#state.client.destroy(); + } + + await this.#initiatePubSubClient(true); + } + + #executeCommand(fn: (client: Client) => T) { + const client = this.#getPubSubClient(); + if (client instanceof RedisClient) { + return fn(client); + } + + return client.then(client => { + // if pubsub was deactivated while connecting + if (client === undefined) return; + + return fn(client); + }).catch(err => { + if (this.#state?.client.isPubSubActive) { + this.#state.client.destroy(); + this.#state = undefined; + } + + throw err; + }); + } + + subscribe( + channels: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + return this.#executeCommand( + client => client.SUBSCRIBE(channels, listener, bufferMode) + ); + } + + #unsubscribe(fn: (client: Client) => Promise) { + return this.#executeCommand(async client => { + const reply = await fn(client); + + if (!client.isPubSubActive) { + client.destroy(); + this.#state = undefined; + } + + return reply; + }); + } + + async unsubscribe( + channels?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ) { + return this.#unsubscribe(client => client.UNSUBSCRIBE(channels, listener, bufferMode)); + } + + async pSubscribe( + patterns: string | Array, + listener: PubSubListener, + bufferMode?: T + ) { + return this.#executeCommand( + client => client.PSUBSCRIBE(patterns, listener, bufferMode) + ); + } + + async pUnsubscribe( + patterns?: string | Array, + listener?: PubSubListener, + bufferMode?: T + ) { + return this.#unsubscribe(client => client.PUNSUBSCRIBE(patterns, listener, bufferMode)); + } + + destroy() { + this.#subscriptions = undefined; + if (this.#state === undefined) return; + + // `connectPromise` already handles the case of `this.#pubSubState = undefined` + if (!this.#state.connectPromise) { + this.#state.client.destroy(); + } + + this.#state = undefined; + } +} diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts new file mode 100644 index 00000000000..25dd4c4371a --- /dev/null +++ b/packages/client/lib/sentinel/test-util.ts @@ -0,0 +1,605 @@ +import { createConnection, Socket } from 'node:net'; +import { setTimeout } from 'node:timers/promises'; +import { once } from 'node:events'; +import { promisify } from 'node:util'; +import { exec } from 'node:child_process'; +import { RedisSentinelOptions, RedisSentinelType } from './types'; +import RedisClient from '../client'; +import RedisSentinel from '.'; +import { RedisArgument, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; +const execAsync = promisify(exec); +import RedisSentinelModule from './module' + +interface ErrorWithCode extends Error { + code: string; +} + +async function isPortAvailable(port: number): Promise { + var socket: Socket | undefined = undefined; + try { + socket = createConnection({ port }); + await once(socket, 'connect'); + } catch (err) { + if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') { + return true; + } + } finally { + if (socket !== undefined) { + socket.end(); + } + } + + return false; +} + +const portIterator = (async function* (): AsyncIterableIterator { + for (let i = 6379; i < 65535; i++) { + if (await isPortAvailable(i)) { + yield i; + } + } + + throw new Error('All ports are in use'); +})(); + +export interface RedisServerDockerConfig { + image: string; + version: string; +} + +export interface RedisServerDocker { + port: number; + dockerId: string; +} + +abstract class DockerBase { + async spawnRedisServerDocker({ image, version }: RedisServerDockerConfig, serverArguments: Array, environment?: string): Promise { + const port = (await portIterator.next()).value; + let cmdLine = `docker run --init -d --network host `; + if (environment !== undefined) { + cmdLine += `-e ${environment} `; + } + cmdLine += `${image}:${version} ${serverArguments.join(' ')}`; + cmdLine = cmdLine.replace('{port}', `--port ${port.toString()}`); + // console.log("spawnRedisServerDocker: cmdLine = " + cmdLine); + const { stdout, stderr } = await execAsync(cmdLine); + + if (!stdout) { + throw new Error(`docker run error - ${stderr}`); + } + + while (await isPortAvailable(port)) { + await setTimeout(50); + } + + return { + port, + dockerId: stdout.trim() + }; + } + + async dockerRemove(dockerId: string): Promise { + try { + await this.dockerStop(dockerId); `` + } catch (err) { + // its ok if stop failed, as we are just going to remove, will just be slower + console.log(`dockerStop failed in remove: ${err}`); + } + + const { stderr } = await execAsync(`docker rm -f ${dockerId}`); + if (stderr) { + console.log("docker rm failed"); + throw new Error(`docker rm error - ${stderr}`); + } + } + + async dockerStop(dockerId: string): Promise { + /* this is an optimization to get around slow docker stop times, but will fail if container is already stopped */ + try { + await execAsync(`docker exec ${dockerId} /bin/bash -c "kill -SIGINT 1"`); + } catch (err) { + /* this will fail if container is already not running, can be ignored */ + } + + let ret = await execAsync(`docker stop ${dockerId}`); + if (ret.stderr) { + throw new Error(`docker stop error - ${ret.stderr}`); + } + } + + async dockerStart(dockerId: string): Promise { + const { stderr } = await execAsync(`docker start ${dockerId}`); + if (stderr) { + throw new Error(`docker start error - ${stderr}`); + } + } +} + +export interface RedisSentinelConfig { + numberOfNodes?: number; + nodeDockerConfig?: RedisServerDockerConfig; + nodeServerArguments?: Array + + numberOfSentinels?: number; + sentinelDockerConfig?: RedisServerDockerConfig; + sentinelServerArgument?: Array + + sentinelName: string; + sentinelQuorum?: number; + + password?: string; +} + +type ArrayElement = + ArrayType extends readonly (infer ElementType)[] ? ElementType : never; + +export interface SentinelController { + getMaster(): Promise; + getMasterPort(): Promise; + getRandomNode(): string; + getRandonNonMasterNode(): Promise; + getNodePort(id: string): number; + getAllNodesPort(): Array; + getSentinelPort(id: string): number; + getAllSentinelsPort(): Array; + getSetinel(i: number): string; + stopNode(id: string): Promise; + restartNode(id: string): Promise; + stopSentinel(id: string): Promise; + restartSentinel(id: string): Promise; + getSentinelClient(opts?: Partial>): RedisSentinelType<{}, {}, {}, 2, {}>; +} + +export class SentinelFramework extends DockerBase { + #nodeList: Awaited> = []; + /* port -> docker info/client */ + #nodeMap: Map>>>; + #sentinelList: Awaited> = []; + /* port -> docker info/client */ + #sentinelMap: Map>>>; + + config: RedisSentinelConfig; + + #spawned: boolean = false; + + get spawned() { + return this.#spawned; + } + + constructor(config: RedisSentinelConfig) { + super(); + + this.config = config; + + this.#nodeMap = new Map>>>(); + this.#sentinelMap = new Map>>>(); + } + + getSentinelClient(opts?: Partial>, errors = true) { + if (opts?.sentinelRootNodes !== undefined) { + throw new Error("cannot specify sentinelRootNodes here"); + } + if (opts?.name !== undefined) { + throw new Error("cannot specify sentinel db name here"); + } + + const options: RedisSentinelOptions = { + name: this.config.sentinelName, + sentinelRootNodes: this.#sentinelList.map((sentinel) => { return { host: '127.0.0.1', port: sentinel.docker.port } }), + passthroughClientErrorEvents: errors + } + + if (this.config.password !== undefined) { + options.nodeClientOptions = {password: this.config.password}; + options.sentinelClientOptions = {password: this.config.password}; + } + + if (opts) { + Object.assign(options, opts); + } + + return RedisSentinel.create(options); + } + + async spawnRedisSentinel() { + if (this.#spawned) { + return; + } + + if (this.#nodeMap.size != 0 || this.#sentinelMap.size != 0) { + throw new Error("inconsistent state with partial setup"); + } + + this.#nodeList = await this.spawnRedisSentinelNodes(); + this.#nodeList.map((value) => this.#nodeMap.set(value.docker.port.toString(), value)); + + this.#sentinelList = await this.spawnRedisSentinelSentinels(); + this.#sentinelList.map((value) => this.#sentinelMap.set(value.docker.port.toString(), value)); + + this.#spawned = true; + } + + async cleanup() { + if (!this.#spawned) { + return; + } + + return Promise.all( + [...this.#nodeMap!.values(), ...this.#sentinelMap!.values()].map( + async ({ docker, client }) => { + if (client.isOpen) { + client.destroy(); + } + this.dockerRemove(docker.dockerId); + } + ) + ).finally(async () => { + this.#spawned = false; + this.#nodeMap.clear(); + this.#sentinelMap.clear(); + }); + } + + protected async spawnRedisSentinelNodeDocker() { + const imageInfo: RedisServerDockerConfig = this.config.nodeDockerConfig ?? { image: "redis/redis-stack-server", version: "latest" }; + const serverArguments: Array = this.config.nodeServerArguments ?? []; + let environment; + if (this.config.password !== undefined) { + environment = `REDIS_ARGS="{port} --requirepass ${this.config.password}"`; + } else { + environment = 'REDIS_ARGS="{port}"'; + } + + const docker = await this.spawnRedisServerDocker(imageInfo, serverArguments, environment); + const client = await RedisClient.create({ + password: this.config.password, + socket: { + port: docker.port + } + }).on("error", () => { }).connect(); + + return { + docker, + client + }; + } + + protected async spawnRedisSentinelNodes() { + const master = await this.spawnRedisSentinelNodeDocker(); + + const promises: Array> = []; + + for (let i = 0; i < (this.config.numberOfNodes ?? 0) - 1; i++) { + promises.push( + this.spawnRedisSentinelNodeDocker().then(async node => { + if (this.config.password !== undefined) { + await node.client.configSet({'masterauth': this.config.password}) + } + await node.client.replicaOf('127.0.0.1', master.docker.port); + return node; + }) + ); + } + + return [ + master, + ...await Promise.all(promises) + ]; + } + + protected async spawnRedisSentinelSentinelDocker() { + const imageInfo: RedisServerDockerConfig = this.config.sentinelDockerConfig ?? { image: "redis", version: "latest" } + let serverArguments: Array; + if (this.config.password === undefined) { + serverArguments = this.config.sentinelServerArgument ?? + [ + "/bin/bash", + "-c", + "\"touch /tmp/sentinel.conf ; /usr/local/bin/redis-sentinel /tmp/sentinel.conf {port} \"" + ]; + } else { + serverArguments = this.config.sentinelServerArgument ?? + [ + "/bin/bash", + "-c", + `"touch /tmp/sentinel.conf ; /usr/local/bin/redis-sentinel /tmp/sentinel.conf {port} --requirepass ${this.config.password}"` + ]; + } + + const docker = await this.spawnRedisServerDocker(imageInfo, serverArguments); + const client = await RedisClient.create({ + modules: RedisSentinelModule, + password: this.config.password, + socket: { + port: docker.port + } + }).on("error", () => { }).connect(); + + return { + docker, + client + }; + } + + protected async spawnRedisSentinelSentinels() { + const quorum = this.config.sentinelQuorum?.toString() ?? "2"; + const node = this.#nodeList[0]; + + const promises: Array> = []; + + for (let i = 0; i < (this.config.numberOfSentinels ?? 3); i++) { + promises.push( + this.spawnRedisSentinelSentinelDocker().then(async sentinel => { + await sentinel.client.sentinel.sentinelMonitor(this.config.sentinelName, '127.0.0.1', node.docker.port.toString(), quorum); + const options: Array<{option: RedisArgument, value: RedisArgument}> = []; + options.push({ option: "down-after-milliseconds", value: "100" }); + options.push({ option: "failover-timeout", value: "5000" }); + if (this.config.password !== undefined) { + options.push({ option: "auth-pass", value: this.config.password }); + } + await sentinel.client.sentinel.sentinelSet(this.config.sentinelName, options) + return sentinel; + }) + ); + } + + return [ + ...await Promise.all(promises) + ] + } + + async getAllRunning() { + for (const port of this.getAllNodesPort()) { + let first = true; + while (await isPortAvailable(port)) { + if (!first) { + console.log(`problematic restart ${port}`); + await setTimeout(500); + } else { + first = false; + } + await this.restartNode(port.toString()); + } + } + + for (const port of this.getAllSentinelsPort()) { + let first = true; + while (await isPortAvailable(port)) { + if (!first) { + await setTimeout(500); + } else { + first = false; + } + await this.restartSentinel(port.toString()); + } + } + } + + async addSentinel() { + const quorum = this.config.sentinelQuorum?.toString() ?? "2"; + const node = this.#nodeList[0]; + const sentinel = await this.spawnRedisSentinelSentinelDocker(); + + await sentinel.client.sentinel.sentinelMonitor(this.config.sentinelName, '127.0.0.1', node.docker.port.toString(), quorum); + const options: Array<{option: RedisArgument, value: RedisArgument}> = []; + options.push({ option: "down-after-milliseconds", value: "100" }); + options.push({ option: "failover-timeout", value: "5000" }); + if (this.config.password !== undefined) { + options.push({ option: "auth-pass", value: this.config.password }); + } + await sentinel.client.sentinel.sentinelSet(this.config.sentinelName, options); + + this.#sentinelList.push(sentinel); + this.#sentinelMap.set(sentinel.docker.port.toString(), sentinel); + } + + async addNode() { + const masterPort = await this.getMasterPort(); + const newNode = await this.spawnRedisSentinelNodeDocker(); + + if (this.config.password !== undefined) { + await newNode.client.configSet({'masterauth': this.config.password}) + } + await newNode.client.replicaOf('127.0.0.1', masterPort); + + this.#nodeList.push(newNode); + this.#nodeMap.set(newNode.docker.port.toString(), newNode); + } + + async getMaster(tracer?: Array): Promise { + for (const sentinel of this.#sentinelMap!.values()) { + let info; + + try { + if (!sentinel.client.isReady) { + continue; + } + + info = await sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); + if (tracer) { + tracer.push('getMaster: master data returned from sentinel'); + tracer.push(JSON.stringify(info, undefined, '\t')) + } + } catch (err) { + console.log("getMaster: sentinelMaster call failed: " + err); + continue; + } + + const master = this.#nodeMap.get(info.port); + if (master === undefined) { + throw new Error(`couldn't find master node for ${info.port}`); + } + + if (tracer) { + tracer.push(`getMaster: master port is either ${info.port} or ${master.docker.port}`); + } + + if (!master.client.isOpen) { + throw new Error(`Sentinel's expected master node (${info.port}) is now down`); + } + + return info.port; + } + + throw new Error("Couldn't get master"); + } + + async getMasterPort(tracer?: Array): Promise { + const data = await this.getMaster(tracer) + + return this.#nodeMap.get(data!)!.docker.port; + } + + getRandomNode() { + return this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)].docker.port.toString(); + } + + async getRandonNonMasterNode(): Promise { + const masterPort = await this.getMasterPort(); + while (true) { + const node = this.#nodeList[Math.floor(Math.random() * this.#nodeList.length)]; + if (node.docker.port != masterPort) { + return node.docker.port.toString(); + } + } + } + + async stopNode(id: string) { +// console.log(`stopping node ${id}`); + let node = this.#nodeMap.get(id); + if (node === undefined) { + throw new Error("unknown node: " + id); + } + + if (node.client.isOpen) { + node.client.destroy(); + } + + return await this.dockerStop(node.docker.dockerId); + } + + async restartNode(id: string) { + let node = this.#nodeMap.get(id); + if (node === undefined) { + throw new Error("unknown node: " + id); + } + + await this.dockerStart(node.docker.dockerId); + if (!node.client.isOpen) { + node.client = await RedisClient.create({ + password: this.config.password, + socket: { + port: node.docker.port + } + }).on("error", () => { }).connect(); + } + } + + async stopSentinel(id: string) { + let sentinel = this.#sentinelMap.get(id); + if (sentinel === undefined) { + throw new Error("unknown sentinel: " + id); + } + + if (sentinel.client.isOpen) { + sentinel.client.destroy(); + } + + return await this.dockerStop(sentinel.docker.dockerId); + } + + async restartSentinel(id: string) { + let sentinel = this.#sentinelMap.get(id); + if (sentinel === undefined) { + throw new Error("unknown sentinel: " + id); + } + + await this.dockerStart(sentinel.docker.dockerId); + if (!sentinel.client.isOpen) { + sentinel.client = await RedisClient.create({ + modules: RedisSentinelModule, + password: this.config.password, + socket: { + port: sentinel.docker.port + } + }).on("error", () => { }).connect(); + } + } + + getNodePort(id: string) { + let node = this.#nodeMap.get(id); + if (node === undefined) { + throw new Error("unknown node: " + id); + } + + return node.docker.port; + } + + getAllNodesPort() { + let ports: Array = []; + for (const node of this.#nodeList) { + ports.push(node.docker.port); + } + + return ports + } + + getAllDockerIds() { + let ids = new Map(); + for (const node of this.#nodeList) { + ids.set(node.docker.dockerId, node.docker.port); + } + + return ids; + } + + getSentinelPort(id: string) { + let sentinel = this.#sentinelMap.get(id); + if (sentinel === undefined) { + throw new Error("unknown sentinel: " + id); + } + + return sentinel.docker.port; + } + + getAllSentinelsPort() { + let ports: Array = []; + for (const sentinel of this.#sentinelList) { + ports.push(sentinel.docker.port); + } + + return ports + } + + getSetinel(i: number): string { + return this.#sentinelList[i].docker.port.toString(); + } + + sentinelSentinels() { + for (const sentinel of this.#sentinelList) { + if (sentinel.client.isReady) { + return sentinel.client.sentinel.sentinelSentinels(this.config.sentinelName); + } + } + } + + sentinelMaster() { + for (const sentinel of this.#sentinelList) { + if (sentinel.client.isReady) { + return sentinel.client.sentinel.sentinelMaster(this.config.sentinelName); + } + } + } + + sentinelReplicas() { + for (const sentinel of this.#sentinelList) { + if (sentinel.client.isReady) { + return sentinel.client.sentinel.sentinelReplicas(this.config.sentinelName); + } + } + } +} diff --git a/packages/client/lib/sentinel/types.ts b/packages/client/lib/sentinel/types.ts new file mode 100644 index 00000000000..1f868ec5177 --- /dev/null +++ b/packages/client/lib/sentinel/types.ts @@ -0,0 +1,175 @@ +import { RedisClientOptions } from '../client'; +import { CommandOptions } from '../client/commands-queue'; +import { CommandSignature, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TypeMapping } from '../RESP/types'; +import COMMANDS from '../commands'; +import RedisSentinel, { RedisSentinelClient } from '.'; +import { RedisTcpSocketOptions } from '../client/socket'; + +export interface RedisNode { + host: string; + port: number; +} + +export interface RedisSentinelOptions< + M extends RedisModules = RedisModules, + F extends RedisFunctions = RedisFunctions, + S extends RedisScripts = RedisScripts, + RESP extends RespVersions = RespVersions, + TYPE_MAPPING extends TypeMapping = TypeMapping +> extends SentinelCommander { + /** + * The sentinel identifier for a particular database cluster + */ + name: string; + /** + * An array of root nodes that are part of the sentinel cluster, which will be used to get the topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the sentinel configuration from the server + */ + sentinelRootNodes: Array; + /** + * The maximum number of times a command will retry due to topology changes. + */ + maxCommandRediscovers?: number; + /** + * The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with + */ + nodeClientOptions?: RedisClientOptions; + /** + * The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with + */ + sentinelClientOptions?: RedisClientOptions; + /** + * The number of clients connected to the master node + */ + masterPoolSize?: number; + /** + * The number of clients connected to each replica node. + * When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes. + */ + replicaPoolSize?: number; + /** + * TODO + */ + scanInterval?: number; + /** + * TODO + */ + passthroughClientErrorEvents?: boolean; + /** + * When `true`, one client will be reserved for the sentinel object. + * When `false`, the sentinel object will wait for the first available client from the pool. + */ + reserveClient?: boolean; +} + +export interface SentinelCommander< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping, + // POLICIES extends CommandPolicies +> extends CommanderConfig { + commandOptions?: CommandOptions; +} + +export type RedisSentinelClientOptions = Omit< + RedisClientOptions, + keyof SentinelCommander +>; + +type WithCommands< + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof typeof COMMANDS]: CommandSignature<(typeof COMMANDS)[P], RESP, TYPE_MAPPING>; +}; + +type WithModules< + M extends RedisModules, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof M]: { + [C in keyof M[P]]: CommandSignature; + }; +}; + +type WithFunctions< + F extends RedisFunctions, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [L in keyof F]: { + [C in keyof F[L]]: CommandSignature; + }; +}; + +type WithScripts< + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> = { + [P in keyof S]: CommandSignature; +}; + +export type RedisSentinelClientType< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {}, +> = ( + RedisSentinelClient & + WithCommands & + WithModules & + WithFunctions & + WithScripts +); + +export type RedisSentinelType< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {}, + // POLICIES extends CommandPolicies = {} +> = ( + RedisSentinel & + WithCommands & + WithModules & + WithFunctions & + WithScripts +); + +export interface SentinelCommandOptions< + TYPE_MAPPING extends TypeMapping = TypeMapping +> extends CommandOptions {} + +export type ProxySentinel = RedisSentinel; +export type ProxySentinelClient = RedisSentinelClient; +export type NamespaceProxySentinel = { _self: ProxySentinel }; +export type NamespaceProxySentinelClient = { _self: ProxySentinelClient }; + +export type NodeInfo = { + ip: any, + port: any, + flags: any, +}; + +export type RedisSentinelEvent = NodeChangeEvent | SizeChangeEvent; + +export type NodeChangeEvent = { + type: "SENTINEL_CHANGE" | "MASTER_CHANGE" | "REPLICA_ADD" | "REPLICA_REMOVE"; + node: RedisNode; +} + +export type SizeChangeEvent = { + type: "SENTINE_LIST_CHANGE"; + size: Number; +} + +export type ClientErrorEvent = { + type: 'MASTER' | 'REPLICA' | 'SENTINEL' | 'PUBSUBPROXY'; + node: RedisNode; + error: Error; +} diff --git a/packages/client/lib/sentinel/utils.ts b/packages/client/lib/sentinel/utils.ts new file mode 100644 index 00000000000..b4d430b1b44 --- /dev/null +++ b/packages/client/lib/sentinel/utils.ts @@ -0,0 +1,114 @@ +import { ArrayReply, Command, RedisFunction, RedisScript, RespVersions, UnwrapReply } from '../RESP/types'; +import { RedisSocketOptions, RedisTcpSocketOptions } from '../client/socket'; +import { functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; +import { NamespaceProxySentinel, NamespaceProxySentinelClient, ProxySentinel, ProxySentinelClient, RedisNode } from './types'; + +/* TODO: should use map interface, would need a transform reply probably? as resp2 is list form, which this depends on */ +export function parseNode(node: Record): RedisNode | undefined{ + + if (node.flags.includes("s_down") || node.flags.includes("disconnected") || node.flags.includes("failover_in_progress")) { + return undefined; + } + + return { host: node.ip, port: Number(node.port) }; +} + +export function createNodeList(nodes: UnwrapReply>>) { + var nodeList: Array = []; + + for (const nodeData of nodes) { + const node = parseNode(nodeData) + if (node === undefined) { + continue; + } + nodeList.push(node); + } + + return nodeList; +} + +export function clientSocketToNode(socket: RedisSocketOptions): RedisNode { + const s = socket as RedisTcpSocketOptions; + + return { + host: s.host!, + port: s.port! + } +} + +export function createCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return async function (this: T, ...args: Array) { + const redisArgs = command.transformArguments(...args); + const typeMapping = this._self.commandOptions?.typeMapping; + + const reply = await this._self.sendCommand( + command.IS_READ_ONLY, + redisArgs, + this._self.commandOptions + ); + + return transformReply ? + transformReply(reply, redisArgs.preserve, typeMapping) : + reply; + }; +} + +export function createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { + const prefix = functionArgumentsPrefix(name, fn), + transformReply = getTransformReply(fn, resp); + return async function (this: T, ...args: Array) { + const fnArgs = fn.transformArguments(...args); + const redisArgs = prefix.concat(fnArgs); + const typeMapping = this._self._self.commandOptions?.typeMapping; + + const reply = await this._self._self.sendCommand( + fn.IS_READ_ONLY, + redisArgs, + this._self._self.commandOptions + ); + + return transformReply ? + transformReply(reply, fnArgs.preserve, typeMapping) : + reply; + } +}; + +export function createModuleCommand(command: Command, resp: RespVersions) { + const transformReply = getTransformReply(command, resp); + return async function (this: T, ...args: Array) { + const redisArgs = command.transformArguments(...args); + const typeMapping = this._self._self.commandOptions?.typeMapping; + + const reply = await this._self._self.sendCommand( + command.IS_READ_ONLY, + redisArgs, + this._self._self.commandOptions + ); + + return transformReply ? + transformReply(reply, redisArgs.preserve, typeMapping) : + reply; + } +}; + +export function createScriptCommand(script: RedisScript, resp: RespVersions) { + const prefix = scriptArgumentsPrefix(script), + transformReply = getTransformReply(script, resp); + return async function (this: T, ...args: Array) { + const scriptArgs = script.transformArguments(...args); + const redisArgs = prefix.concat(scriptArgs); + const typeMapping = this._self.commandOptions?.typeMapping; + + const reply = await this._self.executeScript( + script, + script.IS_READ_ONLY, + redisArgs, + this._self.commandOptions + ); + + return transformReply ? + transformReply(reply, scriptArgs.preserve, typeMapping) : + reply; + }; +} diff --git a/packages/client/lib/sentinel/wait-queue.ts b/packages/client/lib/sentinel/wait-queue.ts new file mode 100644 index 00000000000..138801eb4d9 --- /dev/null +++ b/packages/client/lib/sentinel/wait-queue.ts @@ -0,0 +1,24 @@ +import { SinglyLinkedList } from '../client/linked-list'; + +export class WaitQueue { + #list = new SinglyLinkedList(); + #queue = new SinglyLinkedList<(item: T) => unknown>(); + + push(value: T) { + const resolve = this.#queue.shift(); + if (resolve !== undefined) { + resolve(value); + return; + } + + this.#list.push(value); + } + + shift() { + return this.#list.shift(); + } + + wait() { + return new Promise(resolve => this.#queue.push(resolve)); + } +} diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index fbbac3e0b71..29eb03cb73d 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -1,11 +1,11 @@ import TestUtils from '@redis/test-utils'; import { SinonSpy } from 'sinon'; -import { promiseTimeout } from './utils'; +import { setTimeout } from 'node:timers/promises'; const utils = new TestUtils({ - dockerImageName: 'redis', + dockerImageName: 'redis/redis-stack', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '7.4-rc2' + defaultDockerVersion: '7.4.0-v1' }); export default utils; @@ -15,49 +15,55 @@ const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? []; export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: [...DEBUG_MODE_ARGS] - }, - PASSWORD: { - serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], - clientOptions: { - password: 'password' - } - } + SERVERS: { + OPEN: { + serverArguments: [...DEBUG_MODE_ARGS] + }, + PASSWORD: { + serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], + clientOptions: { + password: 'password' + } + } + }, + CLUSTERS: { + OPEN: { + serverArguments: [...DEBUG_MODE_ARGS] }, - CLUSTERS: { - OPEN: { - serverArguments: [...DEBUG_MODE_ARGS] - }, - PASSWORD: { - serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], - clusterConfiguration: { - defaults: { - password: 'password' - } - } - }, - WITH_REPLICAS: { - serverArguments: [...DEBUG_MODE_ARGS], - numberOfMasters: 2, - numberOfReplicas: 1, - clusterConfiguration: { - useReplicas: true - } + PASSWORD: { + serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], + clusterConfiguration: { + defaults: { + password: 'password' } + } + }, + WITH_REPLICAS: { + serverArguments: [...DEBUG_MODE_ARGS], + numberOfMasters: 2, + numberOfReplicas: 1, + clusterConfiguration: { + useReplicas: true + } } + } }; export async function waitTillBeenCalled(spy: SinonSpy): Promise { - const start = process.hrtime.bigint(), - calls = spy.callCount; + 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'); - } + do { + if (process.hrtime.bigint() - start > 1_000_000_000) { + throw new Error('Waiting for more than 1 second'); + } - await promiseTimeout(50); - } while (spy.callCount === calls); + await setTimeout(50); + } while (spy.callCount === calls); } + +export const BLOCKING_MIN_VALUE = ( + utils.isVersionGreaterThan([7]) ? Number.MIN_VALUE : + utils.isVersionGreaterThan([6]) ? 0.01 : + 1 +); diff --git a/packages/client/lib/utils.ts b/packages/client/lib/utils.ts deleted file mode 100644 index 55bed419813..00000000000 --- a/packages/client/lib/utils.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function promiseTimeout(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); -} diff --git a/packages/client/package.json b/packages/client/package.json index e344edd52c3..cb82f67bd53 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,42 +1,26 @@ { "name": "@redis/client", - "version": "1.6.0", + "version": "2.0.0-next.4", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", "files": [ - "dist/" + "dist/", + "!dist/tsconfig.tsbuildinfo" ], "scripts": { - "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'", - "build": "tsc", - "lint": "eslint ./*.ts ./lib/**/*.ts", - "documentation": "typedoc" + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "dependencies": { - "cluster-key-slot": "1.1.2", - "generic-pool": "3.9.0", - "yallist": "4.0.0" + "cluster-key-slot": "1.1.2" }, "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "@types/sinon": "^10.0.16", - "@types/yallist": "^4.0.1", - "@typescript-eslint/eslint-plugin": "^6.7.2", - "@typescript-eslint/parser": "^6.7.2", - "eslint": "^8.49.0", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "sinon": "^16.0.0", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@types/sinon": "^17.0.3", + "sinon": "^17.0.1" }, "engines": { - "node": ">=14" + "node": ">= 18" }, "repository": { "type": "git", diff --git a/packages/client/tsconfig.json b/packages/client/tsconfig.json index c71595c5702..8caa47300d4 100644 --- a/packages/client/tsconfig.json +++ b/packages/client/tsconfig.json @@ -5,16 +5,13 @@ }, "include": [ "./index.ts", - "./lib/**/*.ts", - "./package.json" + "./lib/**/*.ts" ], "exclude": [ "./lib/test-utils.ts", - "./lib/**/*.spec.ts" + "./lib/**/*.spec.ts", + "./lib/sentinel/test-util.ts" ], - "ts-node": { - "transpileOnly": true - }, "typedocOptions": { "entryPoints": [ "./index.ts", diff --git a/packages/graph/.release-it.json b/packages/graph/.release-it.json index 530d8f355d4..7797dd0b4dd 100644 --- a/packages/graph/.release-it.json +++ b/packages/graph/.release-it.json @@ -5,6 +5,7 @@ "tagAnnotation": "Release ${tagName}" }, "npm": { + "versionArgs": ["--workspaces-update=false"], "publishArgs": ["--access", "public"] } } diff --git a/packages/graph/lib/commands/CONFIG_GET.spec.ts b/packages/graph/lib/commands/CONFIG_GET.spec.ts index 6e1fa74e219..42c7739f5d7 100644 --- a/packages/graph/lib/commands/CONFIG_GET.spec.ts +++ b/packages/graph/lib/commands/CONFIG_GET.spec.ts @@ -1,22 +1,22 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CONFIG_GET'; +import CONFIG_GET from './CONFIG_GET'; -describe('CONFIG GET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('TIMEOUT'), - ['GRAPH.CONFIG', 'GET', 'TIMEOUT'] - ); - }); +describe('GRAPH.CONFIG GET', () => { + it('transformArguments', () => { + assert.deepEqual( + CONFIG_GET.transformArguments('TIMEOUT'), + ['GRAPH.CONFIG', 'GET', 'TIMEOUT'] + ); + }); - testUtils.testWithClient('client.graph.configGet', async client => { - assert.deepEqual( - await client.graph.configGet('TIMEOUT'), - [ - 'TIMEOUT', - 0 - ] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.graph.configGet', async client => { + assert.deepEqual( + await client.graph.configGet('TIMEOUT'), + [ + 'TIMEOUT', + 0 + ] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/CONFIG_GET.ts b/packages/graph/lib/commands/CONFIG_GET.ts index ce80a1148ed..c7ed037e1a1 100644 --- a/packages/graph/lib/commands/CONFIG_GET.ts +++ b/packages/graph/lib/commands/CONFIG_GET.ts @@ -1,12 +1,15 @@ -export const IS_READ_ONLY = true; +import { RedisArgument, TuplesReply, ArrayReply, BlobStringReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(configKey: string): Array { - return ['GRAPH.CONFIG', 'GET', configKey]; -} - -type ConfigItem = [ - configKey: string, - value: number -]; +type ConfigItemReply = TuplesReply<[ + configKey: BlobStringReply, + value: NumberReply +]>; -export declare function transformReply(): ConfigItem | Array; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(configKey: RedisArgument) { + return ['GRAPH.CONFIG', 'GET', configKey]; + }, + transformReply: undefined as unknown as () => ConfigItemReply | ArrayReply +} as const satisfies Command; diff --git a/packages/graph/lib/commands/CONFIG_SET.spec.ts b/packages/graph/lib/commands/CONFIG_SET.spec.ts index 51dce0a8cd9..5ed51e78a29 100644 --- a/packages/graph/lib/commands/CONFIG_SET.spec.ts +++ b/packages/graph/lib/commands/CONFIG_SET.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CONFIG_SET'; +import CONFIG_SET from './CONFIG_SET'; -describe('CONFIG SET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('TIMEOUT', 0), - ['GRAPH.CONFIG', 'SET', 'TIMEOUT', '0'] - ); - }); +describe('GRAPH.CONFIG SET', () => { + it('transformArguments', () => { + assert.deepEqual( + CONFIG_SET.transformArguments('TIMEOUT', 0), + ['GRAPH.CONFIG', 'SET', 'TIMEOUT', '0'] + ); + }); - testUtils.testWithClient('client.graph.configSet', async client => { - assert.equal( - await client.graph.configSet('TIMEOUT', 0), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.graph.configSet', async client => { + assert.equal( + await client.graph.configSet('TIMEOUT', 0), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/CONFIG_SET.ts b/packages/graph/lib/commands/CONFIG_SET.ts index ac81449ad15..ba23ac2f1a7 100644 --- a/packages/graph/lib/commands/CONFIG_SET.ts +++ b/packages/graph/lib/commands/CONFIG_SET.ts @@ -1,10 +1,15 @@ -export function transformArguments(configKey: string, value: number): Array { +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(configKey: RedisArgument, value: number) { return [ - 'GRAPH.CONFIG', - 'SET', - configKey, - value.toString() + 'GRAPH.CONFIG', + 'SET', + configKey, + value.toString() ]; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/graph/lib/commands/DELETE.spec.ts b/packages/graph/lib/commands/DELETE.spec.ts index e51ac2bfab8..6fe24fd827a 100644 --- a/packages/graph/lib/commands/DELETE.spec.ts +++ b/packages/graph/lib/commands/DELETE.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DELETE'; +import DELETE from './DELETE'; -describe('', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['GRAPH.DELETE', 'key'] - ); - }); +describe('GRAPH.DELETE', () => { + it('transformArguments', () => { + assert.deepEqual( + DELETE.transformArguments('key'), + ['GRAPH.DELETE', 'key'] + ); + }); - testUtils.testWithClient('client.graph.delete', async client => { - await client.graph.query('key', 'RETURN 1'); + testUtils.testWithClient('client.graph.delete', async client => { + const [, reply] = await Promise.all([ + client.graph.query('key', 'RETURN 1'), + client.graph.delete('key') + ]); - assert.equal( - typeof await client.graph.delete('key'), - 'string' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(typeof reply, 'string'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/DELETE.ts b/packages/graph/lib/commands/DELETE.ts index 240708143c6..f5f99fb92cc 100644 --- a/packages/graph/lib/commands/DELETE.ts +++ b/packages/graph/lib/commands/DELETE.ts @@ -1,7 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument) { return ['GRAPH.DELETE', key]; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => BlobStringReply +} as const satisfies Command; diff --git a/packages/graph/lib/commands/EXPLAIN.spec.ts b/packages/graph/lib/commands/EXPLAIN.spec.ts index 86d89b212cb..04bf838a4de 100644 --- a/packages/graph/lib/commands/EXPLAIN.spec.ts +++ b/packages/graph/lib/commands/EXPLAIN.spec.ts @@ -1,21 +1,23 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './EXPLAIN'; +import EXPLAIN from './EXPLAIN'; -describe('EXPLAIN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'RETURN 0'), - ['GRAPH.EXPLAIN', 'key', 'RETURN 0'] - ); - }); +describe('GRAPH.EXPLAIN', () => { + it('transformArguments', () => { + assert.deepEqual( + EXPLAIN.transformArguments('key', 'RETURN 0'), + ['GRAPH.EXPLAIN', 'key', 'RETURN 0'] + ); + }); - testUtils.testWithClient('client.graph.explain', async client => { - const [, reply] = await Promise.all([ - client.graph.query('key', 'RETURN 0'), // make sure to create a graph first - client.graph.explain('key', 'RETURN 0') - ]); - assert.ok(Array.isArray(reply)); - assert.ok(!reply.find(x => typeof x !== 'string')); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.graph.explain', async client => { + const [, reply] = await Promise.all([ + client.graph.query('key', 'RETURN 0'), // make sure to create a graph first + client.graph.explain('key', 'RETURN 0') + ]); + assert.ok(Array.isArray(reply)); + for (const item of reply) { + assert.equal(typeof item, 'string'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/EXPLAIN.ts b/packages/graph/lib/commands/EXPLAIN.ts index ebea9ca900d..99a73bf04bf 100644 --- a/packages/graph/lib/commands/EXPLAIN.ts +++ b/packages/graph/lib/commands/EXPLAIN.ts @@ -1,9 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string, query: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, query: RedisArgument) { return ['GRAPH.EXPLAIN', key, query]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/graph/lib/commands/LIST.spec.ts b/packages/graph/lib/commands/LIST.spec.ts index d4fab0358b9..36745efc470 100644 --- a/packages/graph/lib/commands/LIST.spec.ts +++ b/packages/graph/lib/commands/LIST.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './LIST'; +import LIST from './LIST'; -describe('LIST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['GRAPH.LIST'] - ); - }); +describe('GRAPH.LIST', () => { + it('transformArguments', () => { + assert.deepEqual( + LIST.transformArguments(), + ['GRAPH.LIST'] + ); + }); - testUtils.testWithClient('client.graph.list', async client => { - assert.deepEqual( - await client.graph.list(), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.graph.list', async client => { + assert.deepEqual( + await client.graph.list(), + [] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/LIST.ts b/packages/graph/lib/commands/LIST.ts index 1939d43d889..01a868854be 100644 --- a/packages/graph/lib/commands/LIST.ts +++ b/packages/graph/lib/commands/LIST.ts @@ -1,7 +1,10 @@ -export const IS_READ_ONLY = true; +import { ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { return ['GRAPH.LIST']; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/graph/lib/commands/PROFILE.spec.ts b/packages/graph/lib/commands/PROFILE.spec.ts index 80857eb0ab9..a758365d56e 100644 --- a/packages/graph/lib/commands/PROFILE.spec.ts +++ b/packages/graph/lib/commands/PROFILE.spec.ts @@ -1,18 +1,20 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './PROFILE'; +import PROFILE from './PROFILE'; -describe('PROFILE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'RETURN 0'), - ['GRAPH.PROFILE', 'key', 'RETURN 0'] - ); - }); +describe('GRAPH.PROFILE', () => { + it('transformArguments', () => { + assert.deepEqual( + PROFILE.transformArguments('key', 'RETURN 0'), + ['GRAPH.PROFILE', 'key', 'RETURN 0'] + ); + }); - testUtils.testWithClient('client.graph.profile', async client => { - const reply = await client.graph.profile('key', 'RETURN 0'); - assert.ok(Array.isArray(reply)); - assert.ok(!reply.find(x => typeof x !== 'string')); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.graph.profile', async client => { + const reply = await client.graph.profile('key', 'RETURN 0'); + assert.ok(Array.isArray(reply)); + for (const item of reply) { + assert.equal(typeof item, 'string'); + } + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/PROFILE.ts b/packages/graph/lib/commands/PROFILE.ts index c964452f497..2aa1e83dfb0 100644 --- a/packages/graph/lib/commands/PROFILE.ts +++ b/packages/graph/lib/commands/PROFILE.ts @@ -1,9 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; - -export function transformArguments(key: string, query: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, query: RedisArgument) { return ['GRAPH.PROFILE', key, query]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/graph/lib/commands/QUERY.spec.ts b/packages/graph/lib/commands/QUERY.spec.ts index c8a9a20372b..62c9bcaaefe 100644 --- a/packages/graph/lib/commands/QUERY.spec.ts +++ b/packages/graph/lib/commands/QUERY.spec.ts @@ -1,17 +1,63 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './QUERY'; +import QUERY from './QUERY'; -describe('QUERY', () => { - it('transformArguments', () => { +describe('GRAPH.QUERY', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + QUERY.transformArguments('key', 'query'), + ['GRAPH.QUERY', 'key', 'query'] + ); + }); + + describe('params', () => { + it('all types', () => { assert.deepEqual( - transformArguments('key', 'query'), - ['GRAPH.QUERY', 'key', 'query'] + QUERY.transformArguments('key', 'query', { + params: { + null: null, + string: '"\\', + number: 0, + boolean: false, + array: [0], + object: {a: 0} + } + }), + ['GRAPH.QUERY', 'key', 'CYPHER null=null string="\\"\\\\" number=0 boolean=false array=[0] object={a:0} query'] ); + }); + + it('TypeError', () => { + assert.throws(() => { + QUERY.transformArguments('key', 'query', { + params: { + a: Symbol() + } + }) + }, TypeError); + }); }); - - testUtils.testWithClient('client.graph.query', async client => { - const { data } = await client.graph.query('key', 'RETURN 0'); - assert.deepEqual(data, [[0]]); - }, GLOBAL.SERVERS.OPEN); + + it('TIMEOUT', () => { + assert.deepEqual( + QUERY.transformArguments('key', 'query', { + TIMEOUT: 1 + }), + ['GRAPH.QUERY', 'key', 'query', 'TIMEOUT', '1'] + ); + }); + + it('compact', () => { + assert.deepEqual( + QUERY.transformArguments('key', 'query', undefined, true), + ['GRAPH.QUERY', 'key', 'query', '--compact'] + ); + }); + }); + + testUtils.testWithClient('client.graph.query', async client => { + const { data } = await client.graph.query('key', 'RETURN 0'); + assert.deepEqual(data, [[0]]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/QUERY.ts b/packages/graph/lib/commands/QUERY.ts index 741cc6a3601..8a052354610 100644 --- a/packages/graph/lib/commands/QUERY.ts +++ b/packages/graph/lib/commands/QUERY.ts @@ -1,55 +1,102 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands/index'; -import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export function transformArguments( - graph: RedisCommandArgument, - query: RedisCommandArgument, - options?: QueryOptionsBackwardCompatible, - compact?: boolean -): RedisCommandArguments { - return pushQueryArguments( - ['GRAPH.QUERY'], - graph, - query, - options, - compact - ); -} +import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, NullReply, TuplesReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -type Headers = Array; +type Headers = ArrayReply; -type Data = Array; +type Data = ArrayReply; -type Metadata = Array; +type Metadata = ArrayReply; -type QueryRawReply = [ - headers: Headers, - data: Data, - metadata: Metadata +type QueryRawReply = TuplesReply<[ + headers: Headers, + data: Data, + metadata: Metadata ] | [ - metadata: Metadata -]; - -export type QueryReply = { - headers: undefined; - data: undefined; - metadata: Metadata; -} | { - headers: Headers; - data: Data; - metadata: Metadata; + metadata: Metadata +]>; + +type QueryParam = null | string | number | boolean | QueryParams | Array; + +type QueryParams = { + [key: string]: QueryParam; }; -export function transformReply(reply: QueryRawReply): QueryReply { +export interface QueryOptions { + params?: QueryParams; + TIMEOUT?: number; +} + +export function transformQueryArguments( + command: RedisArgument, + graph: RedisArgument, + query: RedisArgument, + options?: QueryOptions, + compact?: boolean +) { + const args = [ + command, + graph, + options?.params ? + `CYPHER ${queryParamsToString(options.params)} ${query}` : + query + ]; + + if (options?.TIMEOUT !== undefined) { + args.push('TIMEOUT', options.TIMEOUT.toString()); + } + + if (compact) { + args.push('--compact'); + } + + return args; +} + +function queryParamsToString(params: QueryParams) { + return Object.entries(params) + .map(([key, value]) => `${key}=${queryParamToString(value)}`) + .join(' '); +} + +function queryParamToString(param: QueryParam): string { + if (param === null) { + return 'null'; + } + + switch (typeof param) { + case 'string': + return `"${param.replace(/["\\]/g, '\\$&')}"`; + + case 'number': + case 'boolean': + return param.toString(); + } + + if (Array.isArray(param)) { + return `[${param.map(queryParamToString).join(',')}]`; + } else if (typeof param === 'object') { + const body = []; + for (const [key, value] of Object.entries(param)) { + body.push(`${key}:${queryParamToString(value)}`); + } + return `{${body.join(',')}}`; + } else { + throw new TypeError(`Unexpected param type ${typeof param} ${param}`) + } +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments: transformQueryArguments.bind(undefined, 'GRAPH.QUERY'), + transformReply(reply: UnwrapReply) { return reply.length === 1 ? { - headers: undefined, - data: undefined, - metadata: reply[0] + headers: undefined, + data: undefined, + metadata: reply[0] } : { - headers: reply[0], - data: reply[1], - metadata: reply[2] + headers: reply[0], + data: reply[1], + metadata: reply[2] }; -} + } +} as const satisfies Command; diff --git a/packages/graph/lib/commands/RO_QUERY.spec.ts b/packages/graph/lib/commands/RO_QUERY.spec.ts index 1d76b1bd652..13829543552 100644 --- a/packages/graph/lib/commands/RO_QUERY.spec.ts +++ b/packages/graph/lib/commands/RO_QUERY.spec.ts @@ -1,20 +1,20 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RO_QUERY'; +import RO_QUERY from './RO_QUERY'; -describe('RO_QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'query'), - ['GRAPH.RO_QUERY', 'key', 'query'] - ); - }); +describe('GRAPH.RO_QUERY', () => { + it('transformArguments', () => { + assert.deepEqual( + RO_QUERY.transformArguments('key', 'query'), + ['GRAPH.RO_QUERY', 'key', 'query'] + ); + }); - testUtils.testWithClient('client.graph.roQuery', async client => { - const [, { data }] = await Promise.all([ - client.graph.query('key', 'RETURN 0'), // make sure to create a graph first - client.graph.roQuery('key', 'RETURN 0') - ]); - assert.deepEqual(data, [[0]]); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.graph.roQuery', async client => { + const [, { data }] = await Promise.all([ + client.graph.query('key', 'RETURN 0'), // make sure to create a graph first + client.graph.roQuery('key', 'RETURN 0') + ]); + assert.deepEqual(data, [[0]]); + }, GLOBAL.SERVERS.OPEN); }); \ No newline at end of file diff --git a/packages/graph/lib/commands/RO_QUERY.ts b/packages/graph/lib/commands/RO_QUERY.ts index d4dda9dee27..5987f511b7d 100644 --- a/packages/graph/lib/commands/RO_QUERY.ts +++ b/packages/graph/lib/commands/RO_QUERY.ts @@ -1,23 +1,9 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushQueryArguments, QueryOptionsBackwardCompatible } from '.'; - -export { FIRST_KEY_INDEX } from './QUERY'; - -export const IS_READ_ONLY = true; - -export function transformArguments( - graph: RedisCommandArgument, - query: RedisCommandArgument, - options?: QueryOptionsBackwardCompatible, - compact?: boolean -): RedisCommandArguments { - return pushQueryArguments( - ['GRAPH.RO_QUERY'], - graph, - query, - options, - compact - ); -} - -export { transformReply } from './QUERY'; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import QUERY, { transformQueryArguments } from './QUERY'; + +export default { + FIRST_KEY_INDEX: QUERY.FIRST_KEY_INDEX, + IS_READ_ONLY: true, + transformArguments: transformQueryArguments.bind(undefined, 'GRAPH.RO_QUERY'), + transformReply: QUERY.transformReply +} as const satisfies Command; diff --git a/packages/graph/lib/commands/SLOWLOG.spec.ts b/packages/graph/lib/commands/SLOWLOG.spec.ts index e3083b994d6..c1c77286a26 100644 --- a/packages/graph/lib/commands/SLOWLOG.spec.ts +++ b/packages/graph/lib/commands/SLOWLOG.spec.ts @@ -1,18 +1,20 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SLOWLOG'; +import SLOWLOG from './SLOWLOG'; -describe('SLOWLOG', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['GRAPH.SLOWLOG', 'key'] - ); - }); +describe('GRAPH.SLOWLOG', () => { + it('transformArguments', () => { + assert.deepEqual( + SLOWLOG.transformArguments('key'), + ['GRAPH.SLOWLOG', 'key'] + ); + }); - testUtils.testWithClient('client.graph.slowLog', async client => { - await client.graph.query('key', 'RETURN 1'); - const reply = await client.graph.slowLog('key'); - assert.equal(reply.length, 1); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.graph.slowLog', async client => { + const [, reply] = await Promise.all([ + client.graph.query('key', 'RETURN 1'), + client.graph.slowLog('key') + ]); + assert.equal(reply.length, 1); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/commands/SLOWLOG.ts b/packages/graph/lib/commands/SLOWLOG.ts index 6ae87af89bf..52927f6040b 100644 --- a/packages/graph/lib/commands/SLOWLOG.ts +++ b/packages/graph/lib/commands/SLOWLOG.ts @@ -1,30 +1,27 @@ -export const IS_READ_ONLY = true; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; +type SlowLogRawReply = ArrayReply>; -export function transformArguments(key: string) { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['GRAPH.SLOWLOG', key]; -} - -type SlowLogRawReply = Array<[ - timestamp: string, - command: string, - query: string, - took: string -]>; - -type SlowLogReply = Array<{ - timestamp: Date; - command: string; - query: string; - took: number; -}>; - -export function transformReply(logs: SlowLogRawReply): SlowLogReply { - return logs.map(([timestamp, command, query, took]) => ({ - timestamp: new Date(Number(timestamp) * 1000), + }, + transformReply(reply: UnwrapReply) { + return reply.map(log => { + const [timestamp, command, query, took] = log as unknown as UnwrapReply; + return { + timestamp: Number(timestamp), command, query, took: Number(took) - })); -} + }; + }); + } +} as const satisfies Command; diff --git a/packages/graph/lib/commands/index.spec.ts b/packages/graph/lib/commands/index.spec.ts deleted file mode 100644 index a688c49dd39..00000000000 --- a/packages/graph/lib/commands/index.spec.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { strict as assert } from 'assert'; -import { pushQueryArguments } from '.'; - -describe('pushQueryArguments', () => { - it('simple', () => { - assert.deepEqual( - pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query'), - ['GRAPH.QUERY', 'graph', 'query'] - ); - }); - - describe('params', () => { - it('all types', () => { - assert.deepEqual( - pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', { - params: { - null: null, - string: '"\\', - number: 0, - boolean: false, - array: [0], - object: {a: 0} - } - }), - ['GRAPH.QUERY', 'graph', 'CYPHER null=null string="\\"\\\\" number=0 boolean=false array=[0] object={a:0} query'] - ); - }); - - it('TypeError', () => { - assert.throws(() => { - pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', { - params: { - a: undefined as any - } - }) - }, TypeError); - }); - }); - - it('TIMEOUT backward compatible', () => { - assert.deepEqual( - pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', 1), - ['GRAPH.QUERY', 'graph', 'query', 'TIMEOUT', '1'] - ); - }); - - it('TIMEOUT', () => { - assert.deepEqual( - pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', { - TIMEOUT: 1 - }), - ['GRAPH.QUERY', 'graph', 'query', 'TIMEOUT', '1'] - ); - }); - - it('compact', () => { - assert.deepEqual( - pushQueryArguments(['GRAPH.QUERY'], 'graph', 'query', undefined, true), - ['GRAPH.QUERY', 'graph', 'query', '--compact'] - ); - }); -}); diff --git a/packages/graph/lib/commands/index.ts b/packages/graph/lib/commands/index.ts index 2acf9089ee6..e93356aa951 100644 --- a/packages/graph/lib/commands/index.ts +++ b/packages/graph/lib/commands/index.ts @@ -1,114 +1,31 @@ -import * as CONFIG_GET from './CONFIG_GET'; -import * as CONFIG_SET from './CONFIG_SET';; -import * as DELETE from './DELETE'; -import * as EXPLAIN from './EXPLAIN'; -import * as LIST from './LIST'; -import * as PROFILE from './PROFILE'; -import * as QUERY from './QUERY'; -import * as RO_QUERY from './RO_QUERY'; -import * as SLOWLOG from './SLOWLOG'; -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import CONFIG_GET from './CONFIG_GET'; +import CONFIG_SET from './CONFIG_SET';; +import DELETE from './DELETE'; +import EXPLAIN from './EXPLAIN'; +import LIST from './LIST'; +import PROFILE from './PROFILE'; +import QUERY from './QUERY'; +import RO_QUERY from './RO_QUERY'; +import SLOWLOG from './SLOWLOG'; export default { - CONFIG_GET, - configGet: CONFIG_GET, - CONFIG_SET, - configSet: CONFIG_SET, - DELETE, - delete: DELETE, - EXPLAIN, - explain: EXPLAIN, - LIST, - list: LIST, - PROFILE, - profile: PROFILE, - QUERY, - query: QUERY, - RO_QUERY, - roQuery: RO_QUERY, - SLOWLOG, - slowLog: SLOWLOG -}; - -type QueryParam = null | string | number | boolean | QueryParams | Array; - -type QueryParams = { - [key: string]: QueryParam; -}; - -export interface QueryOptions { - params?: QueryParams; - TIMEOUT?: number; -} - -export type QueryOptionsBackwardCompatible = QueryOptions | number; - -export function pushQueryArguments( - args: RedisCommandArguments, - graph: RedisCommandArgument, - query: RedisCommandArgument, - options?: QueryOptionsBackwardCompatible, - compact?: boolean -): RedisCommandArguments { - args.push(graph); - - if (typeof options === 'number') { - args.push(query); - pushTimeout(args, options); - } else { - args.push( - options?.params ? - `CYPHER ${queryParamsToString(options.params)} ${query}` : - query - ); - - if (options?.TIMEOUT !== undefined) { - pushTimeout(args, options.TIMEOUT); - } - } - - if (compact) { - args.push('--compact'); - } - - return args; -} - -function pushTimeout(args: RedisCommandArguments, timeout: number): void { - args.push('TIMEOUT', timeout.toString()); -} - -function queryParamsToString(params: QueryParams): string { - const parts = []; - for (const [key, value] of Object.entries(params)) { - parts.push(`${key}=${queryParamToString(value)}`); - } - return parts.join(' '); -} - -function queryParamToString(param: QueryParam): string { - if (param === null) { - return 'null'; - } - - switch (typeof param) { - case 'string': - return `"${param.replace(/["\\]/g, '\\$&')}"`; - - case 'number': - case 'boolean': - return param.toString(); - } - - if (Array.isArray(param)) { - return `[${param.map(queryParamToString).join(',')}]`; - } else if (typeof param === 'object') { - const body = []; - for (const [key, value] of Object.entries(param)) { - body.push(`${key}:${queryParamToString(value)}`); - } - return `{${body.join(',')}}`; - } else { - throw new TypeError(`Unexpected param type ${typeof param} ${param}`) - } -} + CONFIG_GET, + configGet: CONFIG_GET, + CONFIG_SET, + configSet: CONFIG_SET, + DELETE, + delete: DELETE, + EXPLAIN, + explain: EXPLAIN, + LIST, + list: LIST, + PROFILE, + profile: PROFILE, + QUERY, + query: QUERY, + RO_QUERY, + roQuery: RO_QUERY, + SLOWLOG, + slowLog: SLOWLOG +} as const satisfies RedisCommands; diff --git a/packages/graph/lib/graph.spec.ts b/packages/graph/lib/graph.spec.ts index 495c6d17a8a..ab506c43a4b 100644 --- a/packages/graph/lib/graph.spec.ts +++ b/packages/graph/lib/graph.spec.ts @@ -1,148 +1,148 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from './test-utils'; import Graph from './graph'; describe('Graph', () => { - testUtils.testWithClient('null', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN null AS key'); - - assert.deepEqual( - data, - [{ key: null }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('string', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN "string" AS key'); - - assert.deepEqual( - data, - [{ key: 'string' }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('integer', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN 0 AS key'); - - assert.deepEqual( - data, - [{ key: 0 }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('boolean', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN false AS key'); - - assert.deepEqual( - data, - [{ key: false }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('double', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN 0.1 AS key'); - - assert.deepEqual( - data, - [{ key: 0.1 }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('array', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN [null] AS key'); - - assert.deepEqual( - data, - [{ key: [null] }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('edge', async client => { - const graph = new Graph(client as any, 'graph'); - - // check with and without metadata cache - for (let i = 0; i < 2; i++) { - const { data } = await graph.query('CREATE ()-[edge :edge]->() RETURN edge'); - assert.ok(Array.isArray(data)); - assert.equal(data.length, 1); - assert.equal(typeof data[0].edge.id, 'number'); - assert.equal(data[0].edge.relationshipType, 'edge'); - assert.equal(typeof data[0].edge.sourceId, 'number'); - assert.equal(typeof data[0].edge.destinationId, 'number'); - assert.deepEqual(data[0].edge.properties, {}); - } - - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('node', async client => { - const graph = new Graph(client as any, 'graph'); - - // check with and without metadata cache - for (let i = 0; i < 2; i++) { - const { data } = await graph.query('CREATE (node :node { p: 0 }) RETURN node'); - assert.ok(Array.isArray(data)); - assert.equal(data.length, 1); - assert.equal(typeof data[0].node.id, 'number'); - assert.deepEqual(data[0].node.labels, ['node']); - assert.deepEqual(data[0].node.properties, { p: 0 }); - } - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('path', async client => { - const graph = new Graph(client as any, 'graph'), - [, { data }] = await Promise.all([ - await graph.query('CREATE ()-[:edge]->()'), - await graph.roQuery('MATCH path = ()-[:edge]->() RETURN path') - ]); - - assert.ok(Array.isArray(data)); - assert.equal(data.length, 1); - - assert.ok(Array.isArray(data[0].path.nodes)); - assert.equal(data[0].path.nodes.length, 2); - for (const node of data[0].path.nodes) { - assert.equal(typeof node.id, 'number'); - assert.deepEqual(node.labels, []); - assert.deepEqual(node.properties, {}); - } - - assert.ok(Array.isArray(data[0].path.edges)); - assert.equal(data[0].path.edges.length, 1); - for (const edge of data[0].path.edges) { - assert.equal(typeof edge.id, 'number'); - assert.equal(edge.relationshipType, 'edge'); - assert.equal(typeof edge.sourceId, 'number'); - assert.equal(typeof edge.destinationId, 'number'); - assert.deepEqual(edge.properties, {}); - } - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('map', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN { key: "value" } AS map'); - - assert.deepEqual(data, [{ - map: { - key: 'value' - } - }]); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('point', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN point({ latitude: 1, longitude: 2 }) AS point'); - - assert.deepEqual(data, [{ - point: { - latitude: 1, - longitude: 2 - } - }]); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('null', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN null AS key'); + + assert.deepEqual( + data, + [{ key: null }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('string', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN "string" AS key'); + + assert.deepEqual( + data, + [{ key: 'string' }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('integer', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN 0 AS key'); + + assert.deepEqual( + data, + [{ key: 0 }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('boolean', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN false AS key'); + + assert.deepEqual( + data, + [{ key: false }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('double', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN 0.1 AS key'); + + assert.deepEqual( + data, + [{ key: 0.1 }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('array', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN [null] AS key'); + + assert.deepEqual( + data, + [{ key: [null] }] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('edge', async client => { + const graph = new Graph(client as any, 'graph'); + + // check with and without metadata cache + for (let i = 0; i < 2; i++) { + const { data } = await graph.query('CREATE ()-[edge :edge]->() RETURN edge'); + assert.ok(Array.isArray(data)); + assert.equal(data.length, 1); + assert.equal(typeof data[0].edge.id, 'number'); + assert.equal(data[0].edge.relationshipType, 'edge'); + assert.equal(typeof data[0].edge.sourceId, 'number'); + assert.equal(typeof data[0].edge.destinationId, 'number'); + assert.deepEqual(data[0].edge.properties, {}); + } + + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('node', async client => { + const graph = new Graph(client as any, 'graph'); + + // check with and without metadata cache + for (let i = 0; i < 2; i++) { + const { data } = await graph.query('CREATE (node :node { p: 0 }) RETURN node'); + assert.ok(Array.isArray(data)); + assert.equal(data.length, 1); + assert.equal(typeof data[0].node.id, 'number'); + assert.deepEqual(data[0].node.labels, ['node']); + assert.deepEqual(data[0].node.properties, { p: 0 }); + } + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('path', async client => { + const graph = new Graph(client as any, 'graph'), + [, { data }] = await Promise.all([ + await graph.query('CREATE ()-[:edge]->()'), + await graph.roQuery('MATCH path = ()-[:edge]->() RETURN path') + ]); + + assert.ok(Array.isArray(data)); + assert.equal(data.length, 1); + + assert.ok(Array.isArray(data[0].path.nodes)); + assert.equal(data[0].path.nodes.length, 2); + for (const node of data[0].path.nodes) { + assert.equal(typeof node.id, 'number'); + assert.deepEqual(node.labels, []); + assert.deepEqual(node.properties, {}); + } + + assert.ok(Array.isArray(data[0].path.edges)); + assert.equal(data[0].path.edges.length, 1); + for (const edge of data[0].path.edges) { + assert.equal(typeof edge.id, 'number'); + assert.equal(edge.relationshipType, 'edge'); + assert.equal(typeof edge.sourceId, 'number'); + assert.equal(typeof edge.destinationId, 'number'); + assert.deepEqual(edge.properties, {}); + } + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('map', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN { key: "value" } AS map'); + + assert.deepEqual(data, [{ + map: { + key: 'value' + } + }]); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('point', async client => { + const graph = new Graph(client as any, 'graph'), + { data } = await graph.query('RETURN point({ latitude: 1, longitude: 2 }) AS point'); + + assert.deepEqual(data, [{ + point: { + latitude: 1, + longitude: 2 + } + }]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/graph/lib/graph.ts b/packages/graph/lib/graph.ts index a95338bd8f3..348c8b7155f 100644 --- a/packages/graph/lib/graph.ts +++ b/packages/graph/lib/graph.ts @@ -1,359 +1,359 @@ -import { RedisClientType } from '@redis/client/dist/lib/client/index'; -import { RedisCommandArgument, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/commands'; -import { QueryOptions } from './commands'; -import { QueryReply } from './commands/QUERY'; +import { RedisClientType } from '@redis/client'; +import { RedisArgument, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/RESP/types'; +import QUERY, { QueryOptions } from './commands/QUERY'; interface GraphMetadata { - labels: Array; - relationshipTypes: Array; - propertyKeys: Array; + labels: Array; + relationshipTypes: Array; + propertyKeys: Array; } // https://github.com/RedisGraph/RedisGraph/blob/master/src/resultset/formatters/resultset_formatter.h#L20 enum GraphValueTypes { - UNKNOWN = 0, - NULL = 1, - STRING = 2, - INTEGER = 3, - BOOLEAN = 4, - DOUBLE = 5, - ARRAY = 6, - EDGE = 7, - NODE = 8, - PATH = 9, - MAP = 10, - POINT = 11 + UNKNOWN = 0, + NULL = 1, + STRING = 2, + INTEGER = 3, + BOOLEAN = 4, + DOUBLE = 5, + ARRAY = 6, + EDGE = 7, + NODE = 8, + PATH = 9, + MAP = 10, + POINT = 11 } type GraphEntityRawProperties = Array<[ - id: number, - ...value: GraphRawValue + id: number, + ...value: GraphRawValue ]>; type GraphEdgeRawValue = [ - GraphValueTypes.EDGE, - [ - id: number, - relationshipTypeId: number, - sourceId: number, - destinationId: number, - properties: GraphEntityRawProperties - ] + GraphValueTypes.EDGE, + [ + id: number, + relationshipTypeId: number, + sourceId: number, + destinationId: number, + properties: GraphEntityRawProperties + ] ]; type GraphNodeRawValue = [ - GraphValueTypes.NODE, - [ - id: number, - labelIds: Array, - properties: GraphEntityRawProperties - ] + GraphValueTypes.NODE, + [ + id: number, + labelIds: Array, + properties: GraphEntityRawProperties + ] ]; type GraphPathRawValue = [ - GraphValueTypes.PATH, - [ - nodes: [ - GraphValueTypes.ARRAY, - Array - ], - edges: [ - GraphValueTypes.ARRAY, - Array - ] + GraphValueTypes.PATH, + [ + nodes: [ + GraphValueTypes.ARRAY, + Array + ], + edges: [ + GraphValueTypes.ARRAY, + Array ] + ] ]; type GraphMapRawValue = [ - GraphValueTypes.MAP, - Array + GraphValueTypes.MAP, + Array ]; type GraphRawValue = [ - GraphValueTypes.NULL, - null + GraphValueTypes.NULL, + null ] | [ - GraphValueTypes.STRING, - string + GraphValueTypes.STRING, + string ] | [ - GraphValueTypes.INTEGER, - number + GraphValueTypes.INTEGER, + number ] | [ - GraphValueTypes.BOOLEAN, - string + GraphValueTypes.BOOLEAN, + string ] | [ - GraphValueTypes.DOUBLE, - string + GraphValueTypes.DOUBLE, + string ] | [ - GraphValueTypes.ARRAY, - Array + GraphValueTypes.ARRAY, + Array ] | GraphEdgeRawValue | GraphNodeRawValue | GraphPathRawValue | GraphMapRawValue | [ - GraphValueTypes.POINT, - [ - latitude: string, - longitude: string - ] + GraphValueTypes.POINT, + [ + latitude: string, + longitude: string + ] ]; type GraphEntityProperties = Record; interface GraphEdge { - id: number; - relationshipType: string; - sourceId: number; - destinationId: number; - properties: GraphEntityProperties; + id: number; + relationshipType: string; + sourceId: number; + destinationId: number; + properties: GraphEntityProperties; } interface GraphNode { - id: number; - labels: Array; - properties: GraphEntityProperties; + id: number; + labels: Array; + properties: GraphEntityProperties; } interface GraphPath { - nodes: Array; - edges: Array; + nodes: Array; + edges: Array; } type GraphMap = { - [key: string]: GraphValue; + [key: string]: GraphValue; }; type GraphValue = null | string | number | boolean | Array | { } | GraphEdge | GraphNode | GraphPath | GraphMap | { - latitude: string; - longitude: string; + latitude: string; + longitude: string; }; -export type GraphReply = Omit & { - data?: Array; +export type GraphReply = { + data?: Array; }; export type GraphClientType = RedisClientType<{ - graph: { - query: typeof import('./commands/QUERY'), - roQuery: typeof import('./commands/RO_QUERY') - } + graph: { + query: typeof QUERY, + roQuery: typeof import('./commands/RO_QUERY.js').default + } }, RedisFunctions, RedisScripts>; export default class Graph { - #client: GraphClientType; - #name: string; - #metadata?: GraphMetadata; - - constructor( - client: GraphClientType, - name: string - ) { - this.#client = client; - this.#name = name; - } - - async query( - query: RedisCommandArgument, - options?: QueryOptions - ) { - return this.#parseReply( - await this.#client.graph.query( - this.#name, - query, - options, - true - ) - ); - } - - async roQuery( - query: RedisCommandArgument, - options?: QueryOptions - ) { - return this.#parseReply( - await this.#client.graph.roQuery( - this.#name, - query, - options, - true - ) - ); - } - - #setMetadataPromise?: Promise; - - #updateMetadata(): Promise { - this.#setMetadataPromise ??= this.#setMetadata() - .finally(() => this.#setMetadataPromise = undefined); - return this.#setMetadataPromise; - } - - // DO NOT use directly, use #updateMetadata instead - async #setMetadata(): Promise { - const [labels, relationshipTypes, propertyKeys] = await Promise.all([ - this.#client.graph.roQuery(this.#name, 'CALL db.labels()'), - this.#client.graph.roQuery(this.#name, 'CALL db.relationshipTypes()'), - this.#client.graph.roQuery(this.#name, 'CALL db.propertyKeys()') - ]); - - this.#metadata = { - labels: this.#cleanMetadataArray(labels.data as Array<[string]>), - relationshipTypes: this.#cleanMetadataArray(relationshipTypes.data as Array<[string]>), - propertyKeys: this.#cleanMetadataArray(propertyKeys.data as Array<[string]>) - }; - - return this.#metadata; - } - - #cleanMetadataArray(arr: Array<[string]>): Array { - return arr.map(([value]) => value); - } - - #getMetadata( - key: T, - id: number - ): GraphMetadata[T][number] | Promise { - return this.#metadata?.[key][id] ?? this.#getMetadataAsync(key, id); - } - - // DO NOT use directly, use #getMetadata instead - async #getMetadataAsync( - key: T, - id: number - ): Promise { - const value = (await this.#updateMetadata())[key][id]; - if (value === undefined) throw new Error(`Cannot find value from ${key}[${id}]`); + #client: GraphClientType; + #name: string; + #metadata?: GraphMetadata; + + constructor( + client: GraphClientType, + name: string + ) { + this.#client = client; + this.#name = name; + } + + async query( + query: RedisArgument, + options?: QueryOptions + ) { + return this.#parseReply( + await this.#client.graph.query( + this.#name, + query, + options, + true + ) + ); + } + + async roQuery( + query: RedisArgument, + options?: QueryOptions + ) { + return this.#parseReply( + await this.#client.graph.roQuery( + this.#name, + query, + options, + true + ) + ); + } + + #setMetadataPromise?: Promise; + + #updateMetadata(): Promise { + this.#setMetadataPromise ??= this.#setMetadata() + .finally(() => this.#setMetadataPromise = undefined); + return this.#setMetadataPromise; + } + + // DO NOT use directly, use #updateMetadata instead + async #setMetadata(): Promise { + const [labels, relationshipTypes, propertyKeys] = await Promise.all([ + this.#client.graph.roQuery(this.#name, 'CALL db.labels()'), + this.#client.graph.roQuery(this.#name, 'CALL db.relationshipTypes()'), + this.#client.graph.roQuery(this.#name, 'CALL db.propertyKeys()') + ]); + + this.#metadata = { + labels: this.#cleanMetadataArray(labels.data as Array<[string]>), + relationshipTypes: this.#cleanMetadataArray(relationshipTypes.data as Array<[string]>), + propertyKeys: this.#cleanMetadataArray(propertyKeys.data as Array<[string]>) + }; + + return this.#metadata; + } + + #cleanMetadataArray(arr: Array<[string]>): Array { + return arr.map(([value]) => value); + } + + #getMetadata( + key: T, + id: number + ): GraphMetadata[T][number] | Promise { + return this.#metadata?.[key][id] ?? this.#getMetadataAsync(key, id); + } + + // DO NOT use directly, use #getMetadata instead + async #getMetadataAsync( + key: T, + id: number + ): Promise { + const value = (await this.#updateMetadata())[key][id]; + if (value === undefined) throw new Error(`Cannot find value from ${key}[${id}]`); + return value; + } + + // TODO: reply type + async #parseReply(reply: any): Promise> { + if (!reply.data) return reply; + + const promises: Array> = [], + parsed = { + metadata: reply.metadata, + data: reply.data!.map((row: any) => { + const data: Record = {}; + for (let i = 0; i < row.length; i++) { + data[reply.headers[i][1]] = this.#parseValue(row[i], promises); + } + + return data as unknown as T; + }) + }; + + if (promises.length) await Promise.all(promises); + + return parsed; + } + + #parseValue([valueType, value]: GraphRawValue, promises: Array>): GraphValue { + switch (valueType) { + case GraphValueTypes.NULL: + return null; + + case GraphValueTypes.STRING: + case GraphValueTypes.INTEGER: return value; - } - async #parseReply(reply: QueryReply): Promise> { - if (!reply.data) return reply; + case GraphValueTypes.BOOLEAN: + return value === 'true'; - const promises: Array> = [], - parsed = { - metadata: reply.metadata, - data: reply.data!.map((row: any) => { - const data: Record = {}; - for (let i = 0; i < row.length; i++) { - data[reply.headers[i][1]] = this.#parseValue(row[i], promises); - } + case GraphValueTypes.DOUBLE: + return parseFloat(value); - return data as unknown as T; - }) - }; + case GraphValueTypes.ARRAY: + return value.map(x => this.#parseValue(x, promises)); - if (promises.length) await Promise.all(promises); + case GraphValueTypes.EDGE: + return this.#parseEdge(value, promises); - return parsed; - } - - #parseValue([valueType, value]: GraphRawValue, promises: Array>): GraphValue { - switch (valueType) { - case GraphValueTypes.NULL: - return null; - - case GraphValueTypes.STRING: - case GraphValueTypes.INTEGER: - return value; - - case GraphValueTypes.BOOLEAN: - return value === 'true'; - - case GraphValueTypes.DOUBLE: - return parseFloat(value); - - case GraphValueTypes.ARRAY: - return value.map(x => this.#parseValue(x, promises)); - - case GraphValueTypes.EDGE: - return this.#parseEdge(value, promises); - - case GraphValueTypes.NODE: - return this.#parseNode(value, promises); - - case GraphValueTypes.PATH: - return { - nodes: value[0][1].map(([, node]) => this.#parseNode(node, promises)), - edges: value[1][1].map(([, edge]) => this.#parseEdge(edge, promises)) - }; - - case GraphValueTypes.MAP: - const map: GraphMap = {}; - for (let i = 0; i < value.length; i++) { - map[value[i++] as string] = this.#parseValue(value[i] as GraphRawValue, promises); - } - - return map; - - case GraphValueTypes.POINT: - return { - latitude: parseFloat(value[0]), - longitude: parseFloat(value[1]) - }; + case GraphValueTypes.NODE: + return this.#parseNode(value, promises); - default: - throw new Error(`unknown scalar type: ${valueType}`); - } - } + case GraphValueTypes.PATH: + return { + nodes: value[0][1].map(([, node]) => this.#parseNode(node, promises)), + edges: value[1][1].map(([, edge]) => this.#parseEdge(edge, promises)) + }; - #parseEdge([ - id, - relationshipTypeId, - sourceId, - destinationId, - properties - ]: GraphEdgeRawValue[1], promises: Array>): GraphEdge { - const edge = { - id, - sourceId, - destinationId, - properties: this.#parseProperties(properties, promises) - } as GraphEdge; - - const relationshipType = this.#getMetadata('relationshipTypes', relationshipTypeId); - if (relationshipType instanceof Promise) { - promises.push( - relationshipType.then(value => edge.relationshipType = value) - ); - } else { - edge.relationshipType = relationshipType; + case GraphValueTypes.MAP: + const map: GraphMap = {}; + for (let i = 0; i < value.length; i++) { + map[value[i++] as string] = this.#parseValue(value[i] as GraphRawValue, promises); } - return edge; - } - - #parseNode([ - id, - labelIds, - properties - ]: GraphNodeRawValue[1], promises: Array>): GraphNode { - const labels = new Array(labelIds.length); - for (let i = 0; i < labelIds.length; i++) { - const value = this.#getMetadata('labels', labelIds[i]); - if (value instanceof Promise) { - promises.push(value.then(value => labels[i] = value)); - } else { - labels[i] = value; - } - } + return map; + case GraphValueTypes.POINT: return { - id, - labels, - properties: this.#parseProperties(properties, promises) + latitude: parseFloat(value[0]), + longitude: parseFloat(value[1]) }; + + default: + throw new Error(`unknown scalar type: ${valueType}`); + } + } + + #parseEdge([ + id, + relationshipTypeId, + sourceId, + destinationId, + properties + ]: GraphEdgeRawValue[1], promises: Array>): GraphEdge { + const edge = { + id, + sourceId, + destinationId, + properties: this.#parseProperties(properties, promises) + } as GraphEdge; + + const relationshipType = this.#getMetadata('relationshipTypes', relationshipTypeId); + if (relationshipType instanceof Promise) { + promises.push( + relationshipType.then(value => edge.relationshipType = value) + ); + } else { + edge.relationshipType = relationshipType; } - #parseProperties(raw: GraphEntityRawProperties, promises: Array>): GraphEntityProperties { - const parsed: GraphEntityProperties = {}; - for (const [id, type, value] of raw) { - const parsedValue = this.#parseValue([type, value] as GraphRawValue, promises), - key = this.#getMetadata('propertyKeys', id); - if (key instanceof Promise) { - promises.push(key.then(key => parsed[key] = parsedValue)); - } else { - parsed[key] = parsedValue; - } - } + return edge; + } + + #parseNode([ + id, + labelIds, + properties + ]: GraphNodeRawValue[1], promises: Array>): GraphNode { + const labels = new Array(labelIds.length); + for (let i = 0; i < labelIds.length; i++) { + const value = this.#getMetadata('labels', labelIds[i]); + if (value instanceof Promise) { + promises.push(value.then(value => labels[i] = value)); + } else { + labels[i] = value; + } + } - return parsed; + return { + id, + labels, + properties: this.#parseProperties(properties, promises) + }; + } + + #parseProperties(raw: GraphEntityRawProperties, promises: Array>): GraphEntityProperties { + const parsed: GraphEntityProperties = {}; + for (const [id, type, value] of raw) { + const parsedValue = this.#parseValue([type, value] as GraphRawValue, promises), + key = this.#getMetadata('propertyKeys', id); + if (key instanceof Promise) { + promises.push(key.then(key => parsed[key] = parsedValue)); + } else { + parsed[key] = parsedValue; + } } + + return parsed; + } } diff --git a/packages/graph/lib/test-utils.ts b/packages/graph/lib/test-utils.ts index 56c0af56a2e..2aa9384dbe6 100644 --- a/packages/graph/lib/test-utils.ts +++ b/packages/graph/lib/test-utils.ts @@ -2,19 +2,20 @@ import TestUtils from '@redis/test-utils'; import RedisGraph from '.'; export default new TestUtils({ - dockerImageName: 'redislabs/redisgraph', - dockerImageVersionArgument: 'redisgraph-version' + dockerImageName: 'redis/redis-stack', + dockerImageVersionArgument: 'redisgraph-version', + defaultDockerVersion: '7.4.0-v1' }); export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: ['--loadmodule /usr/lib/redis/modules/redisgraph.so'], - clientOptions: { - modules: { - graph: RedisGraph - } - } + SERVERS: { + OPEN: { + serverArguments: [], + clientOptions: { + modules: { + graph: RedisGraph } + } } + } }; diff --git a/packages/graph/package.json b/packages/graph/package.json index 95cce6b8a86..54b6aad6493 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -1,30 +1,24 @@ { "name": "@redis/graph", - "version": "1.1.1", + "version": "2.0.0-next.2", "license": "MIT", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/lib/index.js", + "types": "./dist/lib/index.d.ts", "files": [ - "dist/" + "dist/", + "!dist/tsconfig.tsbuildinfo" ], "scripts": { - "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'", - "build": "tsc", - "documentation": "typedoc" + "test-disable": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" }, "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "repository": { "type": "git", diff --git a/packages/json/.npmignore b/packages/json/.npmignore deleted file mode 100644 index bbef2b404fb..00000000000 --- a/packages/json/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -.nyc_output/ -coverage/ -lib/ -.nycrc.json -.release-it.json -tsconfig.json diff --git a/packages/json/.release-it.json b/packages/json/.release-it.json index ab495a49b13..8de2f3696e3 100644 --- a/packages/json/.release-it.json +++ b/packages/json/.release-it.json @@ -5,6 +5,7 @@ "tagAnnotation": "Release ${tagName}" }, "npm": { + "versionArgs": ["--workspaces-update=false"], "publishArgs": ["--access", "public"] } } diff --git a/packages/json/README.md b/packages/json/README.md index e7f70174116..86996a68370 100644 --- a/packages/json/README.md +++ b/packages/json/README.md @@ -1,12 +1,14 @@ # @redis/json -This package provides support for the [RedisJSON](https://redis.io/docs/stack/json/) module, which adds JSON as a native data type to Redis. It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RedisJSON commands. +This package provides support for the [RedisJSON](https://redis.io/docs/data-types/json/) module, which adds JSON as a native data type to Redis. -To use these extra commands, your Redis server must have the RedisJSON module installed. +Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis). + +:warning: To use these extra commands, your Redis server must have the RedisJSON module installed. ## Usage -For a complete example, see [`managing-json.js`](https://github.com/redis/node-redis/blob/master/examples/managing-json.js) in the Node Redis examples folder. +For a complete example, see [`managing-json.js`](https://github.com/redis/node-redis/blob/master/examples/managing-json.js) in the [examples folder](https://github.com/redis/node-redis/tree/master/examples). ### Storing JSON Documents in Redis @@ -15,33 +17,27 @@ The [`JSON.SET`](https://redis.io/commands/json.set/) command stores a JSON valu Here, we'll store a JSON document in the root of the Redis key "`mydoc`": ```javascript -import { createClient } from 'redis'; - -... await client.json.set('noderedis:jsondata', '$', { name: 'Roberta McDonald', - pets: [ - { + pets: [{ name: 'Rex', species: 'dog', age: 3, isMammal: true - }, - { + }, { name: 'Goldie', species: 'fish', age: 2, isMammal: false - } - ] + }] }); ``` -For more information about RedisJSON's path syntax, [check out the documentation](https://redis.io/docs/stack/json/path/). +For more information about RedisJSON's path syntax, [check out the documentation](https://redis.io/docs/data-types/json/path/). ### Retrieving JSON Documents from Redis -With RedisJSON, we can retrieve all or part(s) of a JSON document using the [`JSON.GET`](https://redis.io/commands/json.get/) command and one or more JSON Paths. Let's get the name and age of one of the pets: +With RedisJSON, we can retrieve all or part(s) of a JSON document using the [`JSON.GET`](https://redis.io/commands/json.get/) command and one or more JSON Paths. Let's get the name and age of one of the pets: ```javascript const results = await client.json.get('noderedis:jsondata', { diff --git a/packages/json/lib/commands/ARRAPPEND.spec.ts b/packages/json/lib/commands/ARRAPPEND.spec.ts index ab53837a000..3bdd967e237 100644 --- a/packages/json/lib/commands/ARRAPPEND.spec.ts +++ b/packages/json/lib/commands/ARRAPPEND.spec.ts @@ -1,30 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ARRAPPEND'; +import ARRAPPEND from './ARRAPPEND'; -describe('ARRAPPEND', () => { - describe('transformArguments', () => { - it('single JSON', () => { - assert.deepEqual( - transformArguments('key', '$', 1), - ['JSON.ARRAPPEND', 'key', '$', '1'] - ); - }); +describe('JSON.ARRAPPEND', () => { + describe('transformArguments', () => { + it('single element', () => { + assert.deepEqual( + ARRAPPEND.transformArguments('key', '$', 'value'), + ['JSON.ARRAPPEND', 'key', '$', '"value"'] + ); + }); - it('multiple JSONs', () => { - assert.deepEqual( - transformArguments('key', '$', 1, 2), - ['JSON.ARRAPPEND', 'key', '$', '1', '2'] - ); - }); + it('multiple elements', () => { + assert.deepEqual( + ARRAPPEND.transformArguments('key', '$', 1, 2), + ['JSON.ARRAPPEND', 'key', '$', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.json.arrAppend', async client => { - await client.json.set('key', '$', []); + testUtils.testWithClient('client.json.arrAppend', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', []), + client.json.arrAppend('key', '$', 'value') + ]); - assert.deepEqual( - await client.json.arrAppend('key', '$', 1), - [1] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [1]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/ARRAPPEND.ts b/packages/json/lib/commands/ARRAPPEND.ts index 2935d192996..6f486a301d7 100644 --- a/packages/json/lib/commands/ARRAPPEND.ts +++ b/packages/json/lib/commands/ARRAPPEND.ts @@ -1,15 +1,27 @@ import { RedisJSON, transformRedisJsonArgument } from '.'; +import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + path: RedisArgument, + json: RedisJSON, + ...jsons: Array + ) { + const args = new Array(4 + jsons.length); + args[0] = 'JSON.ARRAPPEND'; + args[1] = key; + args[2] = path; + args[3] = transformRedisJsonArgument(json); -export function transformArguments(key: string, path: string, ...jsons: Array): Array { - const args = ['JSON.ARRAPPEND', key, path]; - - for (const json of jsons) { - args.push(transformRedisJsonArgument(json)); + let argsIndex = 4; + for (let i = 0; i < jsons.length; i++) { + args[argsIndex++] = transformRedisJsonArgument(jsons[i]); } return args; -} - -export declare function transformReply(): number | Array; + }, + transformReply: undefined as unknown as () => NumberReply | ArrayReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/ARRINDEX.spec.ts b/packages/json/lib/commands/ARRINDEX.spec.ts index 7a47d67126a..cb946b62515 100644 --- a/packages/json/lib/commands/ARRINDEX.spec.ts +++ b/packages/json/lib/commands/ARRINDEX.spec.ts @@ -1,37 +1,48 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ARRINDEX'; +import ARRINDEX from './ARRINDEX'; -describe('ARRINDEX', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('key', '$', 'json'), - ['JSON.ARRINDEX', 'key', '$', '"json"'] - ); - }); - - it('with start', () => { - assert.deepEqual( - transformArguments('key', '$', 'json', 1), - ['JSON.ARRINDEX', 'key', '$', '"json"', '1'] - ); - }); - - it('with start, end', () => { - assert.deepEqual( - transformArguments('key', '$', 'json', 1, 2), - ['JSON.ARRINDEX', 'key', '$', '"json"', '1', '2'] - ); - }); +describe('JSON.ARRINDEX', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ARRINDEX.transformArguments('key', '$', 'value'), + ['JSON.ARRINDEX', 'key', '$', '"value"'] + ); }); - testUtils.testWithClient('client.json.arrIndex', async client => { - await client.json.set('key', '$', []); + describe('with range', () => { + it('start only', () => { + assert.deepEqual( + ARRINDEX.transformArguments('key', '$', 'value', { + range: { + start: 0 + } + }), + ['JSON.ARRINDEX', 'key', '$', '"value"', '0'] + ); + }); + it('with start and stop', () => { assert.deepEqual( - await client.json.arrIndex('key', '$', 'json'), - [-1] + ARRINDEX.transformArguments('key', '$', 'value', { + range: { + start: 0, + stop: 1 + } + }), + ['JSON.ARRINDEX', 'key', '$', '"value"', '0', '1'] ); - }, GLOBAL.SERVERS.OPEN); + }); + }); + }); + + testUtils.testWithClient('client.json.arrIndex', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', []), + client.json.arrIndex('key', '$', 'value') + ]); + + assert.deepEqual(reply, [-1]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/ARRINDEX.ts b/packages/json/lib/commands/ARRINDEX.ts index 5860b59cb3c..77c54b92522 100644 --- a/packages/json/lib/commands/ARRINDEX.ts +++ b/packages/json/lib/commands/ARRINDEX.ts @@ -1,21 +1,33 @@ +import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; +export interface JsonArrIndexOptions { + range?: { + start: number; + stop?: number; + }; +} -export function transformArguments(key: string, path: string, json: RedisJSON, start?: number, stop?: number): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments( + key: RedisArgument, + path: RedisArgument, + json: RedisJSON, + options?: JsonArrIndexOptions + ) { const args = ['JSON.ARRINDEX', key, path, transformRedisJsonArgument(json)]; - if (start !== undefined && start !== null) { - args.push(start.toString()); + if (options?.range) { + args.push(options.range.start.toString()); - if (stop !== undefined && stop !== null) { - args.push(stop.toString()); - } + if (options.range.stop !== undefined) { + args.push(options.range.stop.toString()); + } } return args; -} - -export declare function transformReply(): number | Array; + }, + transformReply: undefined as unknown as () => NumberReply | ArrayReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/ARRINSERT.spec.ts b/packages/json/lib/commands/ARRINSERT.spec.ts index 4b9d58b2cac..efa824b3738 100644 --- a/packages/json/lib/commands/ARRINSERT.spec.ts +++ b/packages/json/lib/commands/ARRINSERT.spec.ts @@ -1,30 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ARRINSERT'; +import ARRINSERT from './ARRINSERT'; -describe('ARRINSERT', () => { - describe('transformArguments', () => { - it('single JSON', () => { - assert.deepEqual( - transformArguments('key', '$', 0, 'json'), - ['JSON.ARRINSERT', 'key', '$', '0', '"json"'] - ); - }); +describe('JSON.ARRINSERT', () => { + describe('transformArguments', () => { + it('single element', () => { + assert.deepEqual( + ARRINSERT.transformArguments('key', '$', 0, 'value'), + ['JSON.ARRINSERT', 'key', '$', '0', '"value"'] + ); + }); - it('multiple JSONs', () => { - assert.deepEqual( - transformArguments('key', '$', 0, '1', '2'), - ['JSON.ARRINSERT', 'key', '$', '0', '"1"', '"2"'] - ); - }); + it('multiple elements', () => { + assert.deepEqual( + ARRINSERT.transformArguments('key', '$', 0, '1', '2'), + ['JSON.ARRINSERT', 'key', '$', '0', '"1"', '"2"'] + ); }); + }); - testUtils.testWithClient('client.json.arrInsert', async client => { - await client.json.set('key', '$', []); + testUtils.testWithClient('client.json.arrInsert', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', []), + client.json.arrInsert('key', '$', 0, 'value') + ]); - assert.deepEqual( - await client.json.arrInsert('key', '$', 0, 'json'), - [1] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [1]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/ARRINSERT.ts b/packages/json/lib/commands/ARRINSERT.ts index 85857657019..c0891884725 100644 --- a/packages/json/lib/commands/ARRINSERT.ts +++ b/packages/json/lib/commands/ARRINSERT.ts @@ -1,15 +1,29 @@ +import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; -export const FIRST_KEY_INDEX = 1; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + path: RedisArgument, + index: number, + json: RedisJSON, + ...jsons: Array + ) { + const args = new Array(4 + jsons.length); + args[0] = 'JSON.ARRINSERT'; + args[1] = key; + args[2] = path; + args[3] = index.toString(); + args[4] = transformRedisJsonArgument(json); -export function transformArguments(key: string, path: string, index: number, ...jsons: Array): Array { - const args = ['JSON.ARRINSERT', key, path, index.toString()]; - - for (const json of jsons) { - args.push(transformRedisJsonArgument(json)); + let argsIndex = 5; + for (let i = 0; i < jsons.length; i++) { + args[argsIndex++] = transformRedisJsonArgument(jsons[i]); } return args; -} - -export declare function transformReply(): number | Array; + }, + transformReply: undefined as unknown as () => NumberReply | ArrayReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/ARRLEN.spec.ts b/packages/json/lib/commands/ARRLEN.spec.ts index f0a3ec40a4c..5ecb01b2ce5 100644 --- a/packages/json/lib/commands/ARRLEN.spec.ts +++ b/packages/json/lib/commands/ARRLEN.spec.ts @@ -1,30 +1,32 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ARRLEN'; +import ARRLEN from './ARRLEN'; -describe('ARRLEN', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.ARRLEN', 'key'] - ); - }); +describe('JSON.ARRLEN', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ARRLEN.transformArguments('key'), + ['JSON.ARRLEN', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.ARRLEN', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + ARRLEN.transformArguments('key', { + path: '$' + }), + ['JSON.ARRLEN', 'key', '$'] + ); }); + }); - testUtils.testWithClient('client.json.arrLen', async client => { - await client.json.set('key', '$', []); + testUtils.testWithClient('client.json.arrLen', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', []), + client.json.arrLen('key') + ]); - assert.deepEqual( - await client.json.arrLen('key', '$'), - [0] - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 0); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/ARRLEN.ts b/packages/json/lib/commands/ARRLEN.ts index 818397b7f8d..d30032c7d86 100644 --- a/packages/json/lib/commands/ARRLEN.ts +++ b/packages/json/lib/commands/ARRLEN.ts @@ -1,15 +1,20 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; +export interface JsonArrLenOptions { + path?: RedisArgument; +} -export function transformArguments(key: string, path?: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, options?: JsonArrLenOptions) { const args = ['JSON.ARRLEN', key]; - if (path) { - args.push(path); + if (options?.path !== undefined) { + args.push(options.path); } return args; -} - -export declare function transformReply(): number | Array; + }, + transformReply: undefined as unknown as () => NumberReply | ArrayReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/ARRPOP.spec.ts b/packages/json/lib/commands/ARRPOP.spec.ts index 7c2ec365eb6..1b069ba3928 100644 --- a/packages/json/lib/commands/ARRPOP.spec.ts +++ b/packages/json/lib/commands/ARRPOP.spec.ts @@ -1,57 +1,66 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ARRPOP'; - -describe('ARRPOP', () => { - describe('transformArguments', () => { - it('key', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.ARRPOP', 'key'] - ); - }); - - it('key, path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.ARRPOP', 'key', '$'] - ); - }); - - it('key, path, index', () => { - assert.deepEqual( - transformArguments('key', '$', 0), - ['JSON.ARRPOP', 'key', '$', '0'] - ); - }); +import ARRPOP from './ARRPOP'; + +describe('JSON.ARRPOP', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + ARRPOP.transformArguments('key'), + ['JSON.ARRPOP', 'key'] + ); + }); + + it('with path', () => { + assert.deepEqual( + ARRPOP.transformArguments('key', { + path: '$' + }), + ['JSON.ARRPOP', 'key', '$'] + ); }); - describe('client.json.arrPop', () => { - testUtils.testWithClient('null', async client => { - await client.json.set('key', '.', []); - - assert.equal( - await client.json.arrPop('key', '.'), - null - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('with value', async client => { - await client.json.set('key', '.', ['value']); - - assert.equal( - await client.json.arrPop('key', '.'), - 'value' - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('array', async client => { - await client.json.set('key', '$', ['value']); - - assert.deepEqual( - await client.json.arrPop('key', '$'), - ['value'] - ); - }, GLOBAL.SERVERS.OPEN); + it('with path and index', () => { + assert.deepEqual( + ARRPOP.transformArguments('key', { + path: '$', + index: 0 + }), + ['JSON.ARRPOP', 'key', '$', '0'] + ); }); + }); + + describe('client.json.arrPop', () => { + testUtils.testWithClient('without path and value', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', []), + client.json.arrPop('key') + ]); + + assert.equal(reply, null); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('. path with value', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '.', ['value']), + client.json.arrPop('key', { + path: '.' + }) + ]); + + assert.equal(reply, 'value'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('$ path with value', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', ['value']), + client.json.arrPop('key', { + path: '$' + }) + ]); + + assert.deepEqual(reply, ['value']); + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/json/lib/commands/ARRPOP.ts b/packages/json/lib/commands/ARRPOP.ts index 18830c0d314..4eafe9fdde4 100644 --- a/packages/json/lib/commands/ARRPOP.ts +++ b/packages/json/lib/commands/ARRPOP.ts @@ -1,27 +1,32 @@ -import { RedisJSON, transformRedisJsonNullReply } from '.'; +import { RedisArgument, ArrayReply, NullReply, BlobStringReply, Command, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import { isArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { transformRedisJsonNullReply } from '.'; -export const FIRST_KEY_INDEX = 1; +export interface RedisArrPopOptions { + path: RedisArgument; + index?: number; +} -export function transformArguments(key: string, path?: string, index?: number): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: RedisArrPopOptions) { const args = ['JSON.ARRPOP', key]; - if (path) { - args.push(path); + if (options) { + args.push(options.path); - if (index !== undefined && index !== null) { - args.push(index.toString()); - } + if (options.index !== undefined) { + args.push(options.index.toString()); + } } - + return args; -} - -export function transformReply(reply: null | string | Array): null | RedisJSON | Array { - if (reply === null) return null; + }, + transformReply(reply: NullReply | BlobStringReply | ArrayReply) { + return isArrayReply(reply) ? + (reply as unknown as UnwrapReply).map(item => transformRedisJsonNullReply(item)) : + transformRedisJsonNullReply(reply); + } +} as const satisfies Command; - if (Array.isArray(reply)) { - return reply.map(transformRedisJsonNullReply); - } - - return transformRedisJsonNullReply(reply); -} diff --git a/packages/json/lib/commands/ARRTRIM.spec.ts b/packages/json/lib/commands/ARRTRIM.spec.ts index c254e1b6a03..4c2f72aaa50 100644 --- a/packages/json/lib/commands/ARRTRIM.spec.ts +++ b/packages/json/lib/commands/ARRTRIM.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ARRTRIM'; +import ARRTRIM from './ARRTRIM'; -describe('ARRTRIM', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '$', 0, 1), - ['JSON.ARRTRIM', 'key', '$', '0', '1'] - ); - }); +describe('JSON.ARRTRIM', () => { + it('transformArguments', () => { + assert.deepEqual( + ARRTRIM.transformArguments('key', '$', 0, 1), + ['JSON.ARRTRIM', 'key', '$', '0', '1'] + ); + }); - testUtils.testWithClient('client.json.arrTrim', async client => { - await client.json.set('key', '$', []); + testUtils.testWithClient('client.json.arrTrim', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', []), + client.json.arrTrim('key', '$', 0, 1) + ]); - assert.deepEqual( - await client.json.arrTrim('key', '$', 0, 1), - [0] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [0]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/ARRTRIM.ts b/packages/json/lib/commands/ARRTRIM.ts index 2de444eeebd..ab31f159491 100644 --- a/packages/json/lib/commands/ARRTRIM.ts +++ b/packages/json/lib/commands/ARRTRIM.ts @@ -1,7 +1,10 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path: string, start: number, stop: number): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, path: RedisArgument, start: number, stop: number) { return ['JSON.ARRTRIM', key, path, start.toString(), stop.toString()]; -} - -export declare function transformReply(): number | Array; + }, + transformReply: undefined as unknown as () => NumberReply | ArrayReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/CLEAR.spec.ts b/packages/json/lib/commands/CLEAR.spec.ts new file mode 100644 index 00000000000..983e6bec2d9 --- /dev/null +++ b/packages/json/lib/commands/CLEAR.spec.ts @@ -0,0 +1,32 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import CLEAR from './CLEAR'; + +describe('JSON.CLEAR', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + CLEAR.transformArguments('key'), + ['JSON.CLEAR', 'key'] + ); + }); + + it('with path', () => { + assert.deepEqual( + CLEAR.transformArguments('key', { + path: '$' + }), + ['JSON.CLEAR', 'key', '$'] + ); + }); + }); + + testUtils.testWithClient('client.json.clear', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', null), + client.json.clear('key') + ]); + + assert.equal(reply, 0); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/json/lib/commands/CLEAR.ts b/packages/json/lib/commands/CLEAR.ts new file mode 100644 index 00000000000..23e86d900e7 --- /dev/null +++ b/packages/json/lib/commands/CLEAR.ts @@ -0,0 +1,20 @@ +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; + +export interface JsonClearOptions { + path?: RedisArgument; +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: JsonClearOptions) { + const args = ['JSON.CLEAR', key]; + + if (options?.path !== undefined) { + args.push(options.path); + } + + return args; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/DEBUG_MEMORY.spec.ts b/packages/json/lib/commands/DEBUG_MEMORY.spec.ts index 468c994f2f5..c41d07cb27e 100644 --- a/packages/json/lib/commands/DEBUG_MEMORY.spec.ts +++ b/packages/json/lib/commands/DEBUG_MEMORY.spec.ts @@ -1,28 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DEBUG_MEMORY'; +import DEBUG_MEMORY from './DEBUG_MEMORY'; -describe('DEBUG MEMORY', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.DEBUG', 'MEMORY', 'key'] - ); - }); +describe('JSON.DEBUG MEMORY', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + DEBUG_MEMORY.transformArguments('key'), + ['JSON.DEBUG', 'MEMORY', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.DEBUG', 'MEMORY', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + DEBUG_MEMORY.transformArguments('key', { + path: '$' + }), + ['JSON.DEBUG', 'MEMORY', 'key', '$'] + ); }); + }); - testUtils.testWithClient('client.json.arrTrim', async client => { - assert.deepEqual( - await client.json.debugMemory('key', '$'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.debugMemory', async client => { + assert.equal( + await client.json.debugMemory('key'), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/DEBUG_MEMORY.ts b/packages/json/lib/commands/DEBUG_MEMORY.ts index da60b1d9529..c2e730b9dc2 100644 --- a/packages/json/lib/commands/DEBUG_MEMORY.ts +++ b/packages/json/lib/commands/DEBUG_MEMORY.ts @@ -1,13 +1,20 @@ -export const FIRST_KEY_INDEX = 2; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { +export interface JsonDebugMemoryOptions { + path?: RedisArgument; +} + +export default { + FIRST_KEY_INDEX: 2, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: JsonDebugMemoryOptions) { const args = ['JSON.DEBUG', 'MEMORY', key]; - if (path) { - args.push(path); + if (options?.path !== undefined) { + args.push(options.path); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/DEL.spec.ts b/packages/json/lib/commands/DEL.spec.ts index a957b9584ac..18f6a8f2db6 100644 --- a/packages/json/lib/commands/DEL.spec.ts +++ b/packages/json/lib/commands/DEL.spec.ts @@ -1,28 +1,31 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DEL'; +import DEL from './DEL'; -describe('DEL', () => { - describe('transformArguments', () => { - it('key', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.DEL', 'key'] - ); - }); +describe('JSON.DEL', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + DEL.transformArguments('key'), + ['JSON.DEL', 'key'] + ); + }); - it('key, path', () => { - assert.deepEqual( - transformArguments('key', '$.path'), - ['JSON.DEL', 'key', '$.path'] - ); - }); + it('with path', () => { + assert.deepEqual( + DEL.transformArguments('key', { + path: '$.path' + }), + ['JSON.DEL', 'key', '$.path'] + ); }); + }); - testUtils.testWithClient('client.json.del', async client => { - assert.deepEqual( - await client.json.del('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.del', async client => { + assert.equal( + await client.json.del('key'), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); + diff --git a/packages/json/lib/commands/DEL.ts b/packages/json/lib/commands/DEL.ts index 090d4dbe853..f6952a8dc63 100644 --- a/packages/json/lib/commands/DEL.ts +++ b/packages/json/lib/commands/DEL.ts @@ -1,13 +1,20 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { +export interface JsonDelOptions { + path?: RedisArgument +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: JsonDelOptions) { const args = ['JSON.DEL', key]; - if (path) { - args.push(path); + if (options?.path !== undefined) { + args.push(options.path); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/FORGET.spec.ts b/packages/json/lib/commands/FORGET.spec.ts index 923bb997fc8..04066ec43a9 100644 --- a/packages/json/lib/commands/FORGET.spec.ts +++ b/packages/json/lib/commands/FORGET.spec.ts @@ -1,28 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './FORGET'; +import FORGET from './FORGET'; -describe('FORGET', () => { - describe('transformArguments', () => { - it('key', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.FORGET', 'key'] - ); - }); +describe('JSON.FORGET', () => { + describe('transformArguments', () => { + it('key', () => { + assert.deepEqual( + FORGET.transformArguments('key'), + ['JSON.FORGET', 'key'] + ); + }); - it('key, path', () => { - assert.deepEqual( - transformArguments('key', '$.path'), - ['JSON.FORGET', 'key', '$.path'] - ); - }); + it('key, path', () => { + assert.deepEqual( + FORGET.transformArguments('key', { + path: '$.path' + }), + ['JSON.FORGET', 'key', '$.path'] + ); }); + }); - testUtils.testWithClient('client.json.forget', async client => { - assert.deepEqual( - await client.json.forget('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.forget', async client => { + assert.equal( + await client.json.forget('key'), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/FORGET.ts b/packages/json/lib/commands/FORGET.ts index cb2df3d605d..68335ee92e9 100644 --- a/packages/json/lib/commands/FORGET.ts +++ b/packages/json/lib/commands/FORGET.ts @@ -1,13 +1,20 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { +export interface JsonForgetOptions { + path?: RedisArgument; +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: JsonForgetOptions) { const args = ['JSON.FORGET', key]; - if (path) { - args.push(path); + if (options?.path !== undefined) { + args.push(options.path); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/GET.spec.ts b/packages/json/lib/commands/GET.spec.ts index ed831689a93..6d6ff14f67f 100644 --- a/packages/json/lib/commands/GET.spec.ts +++ b/packages/json/lib/commands/GET.spec.ts @@ -1,78 +1,37 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GET'; - -describe('GET', () => { - describe('transformArguments', () => { - describe('path', () => { - it('string', () => { - assert.deepEqual( - transformArguments('key', { path: '$' }), - ['JSON.GET', 'key', '$'] - ); - }); - - it('array', () => { - assert.deepEqual( - transformArguments('key', { path: ['$.1', '$.2'] }), - ['JSON.GET', 'key', '$.1', '$.2'] - ); - }); - }); - - it('key', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.GET', 'key'] - ); - }); - - it('INDENT', () => { - assert.deepEqual( - transformArguments('key', { INDENT: 'indent' }), - ['JSON.GET', 'key', 'INDENT', 'indent'] - ); - }); - - it('NEWLINE', () => { - assert.deepEqual( - transformArguments('key', { NEWLINE: 'newline' }), - ['JSON.GET', 'key', 'NEWLINE', 'newline'] - ); - }); - - it('SPACE', () => { - assert.deepEqual( - transformArguments('key', { SPACE: 'space' }), - ['JSON.GET', 'key', 'SPACE', 'space'] - ); - }); - - it('NOESCAPE', () => { - assert.deepEqual( - transformArguments('key', { NOESCAPE: true }), - ['JSON.GET', 'key', 'NOESCAPE'] - ); - }); - - it('INDENT, NEWLINE, SPACE, NOESCAPE, path', () => { - assert.deepEqual( - transformArguments('key', { - path: '$.path', - INDENT: 'indent', - NEWLINE: 'newline', - SPACE: 'space', - NOESCAPE: true - }), - ['JSON.GET', 'key', '$.path', 'INDENT', 'indent', 'NEWLINE', 'newline', 'SPACE', 'space', 'NOESCAPE'] - ); - }); +import GET from './GET'; + +describe('JSON.GET', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + GET.transformArguments('key'), + ['JSON.GET', 'key'] + ); }); - testUtils.testWithClient('client.json.get', async client => { - assert.equal( - await client.json.get('key'), - null + describe('with path', () => { + it('string', () => { + assert.deepEqual( + GET.transformArguments('key', { path: '$' }), + ['JSON.GET', 'key', '$'] ); - }, GLOBAL.SERVERS.OPEN); + }); + + it('array', () => { + assert.deepEqual( + GET.transformArguments('key', { path: ['$.1', '$.2'] }), + ['JSON.GET', 'key', '$.1', '$.2'] + ); + }); + }); + }); + + testUtils.testWithClient('client.json.get', async client => { + assert.equal( + await client.json.get('key'), + null + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/GET.ts b/packages/json/lib/commands/GET.ts index 21bad09568b..b7bcc52e3cb 100644 --- a/packages/json/lib/commands/GET.ts +++ b/packages/json/lib/commands/GET.ts @@ -1,42 +1,22 @@ -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { transformRedisJsonNullReply } from '.'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -interface GetOptions { - path?: string | Array; - INDENT?: string; - NEWLINE?: string; - SPACE?: string; - NOESCAPE?: true; +export interface JsonGetOptions { + path?: RedisVariadicArgument; } -export function transformArguments(key: string, options?: GetOptions): RedisCommandArguments { - let args: RedisCommandArguments = ['JSON.GET', key]; - - if (options?.path) { - args = pushVerdictArguments(args, options.path); - } - - if (options?.INDENT) { - args.push('INDENT', options.INDENT); - } - - if (options?.NEWLINE) { - args.push('NEWLINE', options.NEWLINE); - } - - if (options?.SPACE) { - args.push('SPACE', options.SPACE); - } +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: JsonGetOptions) { + let args = ['JSON.GET', key]; - if (options?.NOESCAPE) { - args.push('NOESCAPE'); + if (options?.path !== undefined) { + args = pushVariadicArguments(args, options.path); } return args; -} - -export { transformRedisJsonNullReply as transformReply } from '.'; + }, + transformReply: transformRedisJsonNullReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/MERGE.spec.ts b/packages/json/lib/commands/MERGE.spec.ts index ee5e6fff86d..56f5d25e7d5 100644 --- a/packages/json/lib/commands/MERGE.spec.ts +++ b/packages/json/lib/commands/MERGE.spec.ts @@ -1,21 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MERGE'; +import MERGE from './MERGE'; -describe('MERGE', () => { - testUtils.isVersionGreaterThanHook([2, 6]); +describe('JSON.MERGE', () => { + it('transformArguments', () => { + assert.deepEqual( + MERGE.transformArguments('key', '$', 'value'), + ['JSON.MERGE', 'key', '$', '"value"'] + ); + }); - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '$', 1), - ['JSON.MERGE', 'key', '$', '1'] - ); - }); - - testUtils.testWithClient('client.json.merge', async client => { - assert.equal( - await client.json.merge('key', '$', 'json'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.merge', async client => { + assert.equal( + await client.json.merge('key', '$', 'value'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/MERGE.ts b/packages/json/lib/commands/MERGE.ts index 81cce7f006b..90cd080a06e 100644 --- a/packages/json/lib/commands/MERGE.ts +++ b/packages/json/lib/commands/MERGE.ts @@ -1,9 +1,16 @@ +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: string, path: string, json: RedisJSON): Array { - return ['JSON.MERGE', key, path, transformRedisJsonArgument(json)]; -} - -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, path: RedisArgument, value: RedisJSON) { + return [ + 'JSON.MERGE', + key, + path, + transformRedisJsonArgument(value) + ]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/json/lib/commands/MGET.spec.ts b/packages/json/lib/commands/MGET.spec.ts index 456e160dd50..1bfaecd6da9 100644 --- a/packages/json/lib/commands/MGET.spec.ts +++ b/packages/json/lib/commands/MGET.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MGET'; +import MGET from './MGET'; -describe('MGET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(['1', '2'], '$'), - ['JSON.MGET', '1', '2', '$'] - ); - }); +describe('JSON.MGET', () => { + it('transformArguments', () => { + assert.deepEqual( + MGET.transformArguments(['1', '2'], '$'), + ['JSON.MGET', '1', '2', '$'] + ); + }); - testUtils.testWithClient('client.json.mGet', async client => { - assert.deepEqual( - await client.json.mGet(['1', '2'], '$'), - [null, null] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.mGet', async client => { + assert.deepEqual( + await client.json.mGet(['1', '2'], '$'), + [null, null] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/MGET.ts b/packages/json/lib/commands/MGET.ts index 34ca8da289f..a7aea82ac2e 100644 --- a/packages/json/lib/commands/MGET.ts +++ b/packages/json/lib/commands/MGET.ts @@ -1,17 +1,17 @@ -import { RedisJSON, transformRedisJsonNullReply } from '.'; +import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformRedisJsonNullReply } from '.'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments(keys: Array, path: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(keys: Array, path: RedisArgument) { return [ - 'JSON.MGET', - ...keys, - path + 'JSON.MGET', + ...keys, + path ]; -} - -export function transformReply(reply: Array): Array { - return reply.map(transformRedisJsonNullReply); -} + }, + transformReply(reply: UnwrapReply>) { + return reply.map(json => transformRedisJsonNullReply(json)) + } +} as const satisfies Command; diff --git a/packages/json/lib/commands/MSET.spec.ts b/packages/json/lib/commands/MSET.spec.ts index 53d4d822505..360890234c8 100644 --- a/packages/json/lib/commands/MSET.spec.ts +++ b/packages/json/lib/commands/MSET.spec.ts @@ -1,35 +1,35 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MSET'; +import MSET from './MSET'; -describe('MSET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments([{ - key: '1', - path: '$', - value: 1 - }, { - key: '2', - path: '$', - value: '2' - }]), - ['JSON.MSET', '1', '$', '1', '2', '$', '"2"'] - ); - }); +describe('JSON.MSET', () => { + it('transformArguments', () => { + assert.deepEqual( + MSET.transformArguments([{ + key: '1', + path: '$', + value: 1 + }, { + key: '2', + path: '$', + value: '2' + }]), + ['JSON.MSET', '1', '$', '1', '2', '$', '"2"'] + ); + }); - testUtils.testWithClient('client.json.mSet', async client => { - assert.deepEqual( - await client.json.mSet([{ - key: '1', - path: '$', - value: 1 - }, { - key: '2', - path: '$', - value: '2' - }]), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.mSet', async client => { + assert.equal( + await client.json.mSet([{ + key: '1', + path: '$', + value: 1 + }, { + key: '2', + path: '$', + value: '2' + }]), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/MSET.ts b/packages/json/lib/commands/MSET.ts index 67228f264d2..a081bfd543a 100644 --- a/packages/json/lib/commands/MSET.ts +++ b/packages/json/lib/commands/MSET.ts @@ -1,28 +1,28 @@ +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; -import { RedisCommandArgument } from '@redis/client/dist/lib/commands'; -export const FIRST_KEY_INDEX = 1; - -interface JsonMSetItem { - key: RedisCommandArgument; - path: RedisCommandArgument; - value: RedisJSON; +export interface JsonMSetItem { + key: RedisArgument; + path: RedisArgument; + value: RedisJSON; } -export function transformArguments(items: Array): Array { - - const args = new Array(1 + items.length * 3); +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(items: Array) { + const args = new Array(1 + items.length * 3); args[0] = 'JSON.MSET'; let argsIndex = 1; for (let i = 0; i < items.length; i++) { - const item = items[i]; - args[argsIndex++] = item.key; - args[argsIndex++] = item.path; - args[argsIndex++] = transformRedisJsonArgument(item.value); + const item = items[i]; + args[argsIndex++] = item.key; + args[argsIndex++] = item.path; + args[argsIndex++] = transformRedisJsonArgument(item.value); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/json/lib/commands/NUMINCRBY.spec.ts b/packages/json/lib/commands/NUMINCRBY.spec.ts index 56dede68bde..d0bffd2bd2a 100644 --- a/packages/json/lib/commands/NUMINCRBY.spec.ts +++ b/packages/json/lib/commands/NUMINCRBY.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './NUMINCRBY'; +import NUMINCRBY from './NUMINCRBY'; -describe('NUMINCRBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '$', 1), - ['JSON.NUMINCRBY', 'key', '$', '1'] - ); - }); +describe('JSON.NUMINCRBY', () => { + it('transformArguments', () => { + assert.deepEqual( + NUMINCRBY.transformArguments('key', '$', 1), + ['JSON.NUMINCRBY', 'key', '$', '1'] + ); + }); - testUtils.testWithClient('client.json.numIncrBy', async client => { - await client.json.set('key', '$', 0); + testUtils.testWithClient('client.json.numIncrBy', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', 0), + client.json.numIncrBy('key', '$', 1) + ]); - assert.deepEqual( - await client.json.numIncrBy('key', '$', 1), - [1] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [1]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/NUMINCRBY.ts b/packages/json/lib/commands/NUMINCRBY.ts index e3d8887ea3d..65cc7db68a9 100644 --- a/packages/json/lib/commands/NUMINCRBY.ts +++ b/packages/json/lib/commands/NUMINCRBY.ts @@ -1,7 +1,15 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, NumberReply, DoubleReply, NullReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path: string, by: number): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, path: RedisArgument, by: number) { return ['JSON.NUMINCRBY', key, path, by.toString()]; -} - -export { transformNumbersReply as transformReply } from '.'; + }, + transformReply: { + 2: (reply: UnwrapReply) => { + return JSON.parse(reply.toString()) as number | Array; + }, + 3: undefined as unknown as () => ArrayReply + } +} as const satisfies Command; diff --git a/packages/json/lib/commands/NUMMULTBY.spec.ts b/packages/json/lib/commands/NUMMULTBY.spec.ts index 3e2581a3cd8..9767c2b0979 100644 --- a/packages/json/lib/commands/NUMMULTBY.spec.ts +++ b/packages/json/lib/commands/NUMMULTBY.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './NUMMULTBY'; +import NUMMULTBY from './NUMMULTBY'; -describe('NUMMULTBY', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '$', 2), - ['JSON.NUMMULTBY', 'key', '$', '2'] - ); - }); +describe('JSON.NUMMULTBY', () => { + it('transformArguments', () => { + assert.deepEqual( + NUMMULTBY.transformArguments('key', '$', 2), + ['JSON.NUMMULTBY', 'key', '$', '2'] + ); + }); - testUtils.testWithClient('client.json.numMultBy', async client => { - await client.json.set('key', '$', 1); + testUtils.testWithClient('client.json.numMultBy', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', 1), + client.json.numMultBy('key', '$', 2) + ]); - assert.deepEqual( - await client.json.numMultBy('key', '$', 2), - [2] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [2]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/NUMMULTBY.ts b/packages/json/lib/commands/NUMMULTBY.ts index 2082916619a..255685a9a50 100644 --- a/packages/json/lib/commands/NUMMULTBY.ts +++ b/packages/json/lib/commands/NUMMULTBY.ts @@ -1,7 +1,11 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import NUMINCRBY from './NUMINCRBY'; -export function transformArguments(key: string, path: string, by: number): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, path: RedisArgument, by: number) { return ['JSON.NUMMULTBY', key, path, by.toString()]; -} - -export { transformNumbersReply as transformReply } from '.'; + }, + transformReply: NUMINCRBY.transformReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/OBJKEYS.spec.ts b/packages/json/lib/commands/OBJKEYS.spec.ts index 6288c112392..dc984cb2ce9 100644 --- a/packages/json/lib/commands/OBJKEYS.spec.ts +++ b/packages/json/lib/commands/OBJKEYS.spec.ts @@ -1,28 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './OBJKEYS'; +import OBJKEYS from './OBJKEYS'; -describe('OBJKEYS', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.OBJKEYS', 'key'] - ); - }); +describe('JSON.OBJKEYS', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + OBJKEYS.transformArguments('key'), + ['JSON.OBJKEYS', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.OBJKEYS', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + OBJKEYS.transformArguments('key', { + path: '$' + }), + ['JSON.OBJKEYS', 'key', '$'] + ); }); + }); - // testUtils.testWithClient('client.json.objKeys', async client => { - // assert.deepEqual( - // await client.json.objKeys('key', '$'), - // [null] - // ); - // }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.objKeys', async client => { + assert.equal( + await client.json.objKeys('key'), + null + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/OBJKEYS.ts b/packages/json/lib/commands/OBJKEYS.ts index a9465c9160c..fb8ae6bb034 100644 --- a/packages/json/lib/commands/OBJKEYS.ts +++ b/packages/json/lib/commands/OBJKEYS.ts @@ -1,13 +1,20 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { +export interface JsonObjKeysOptions { + path?: RedisArgument; +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: JsonObjKeysOptions) { const args = ['JSON.OBJKEYS', key]; - if (path) { - args.push(path); + if (options?.path !== undefined) { + args.push(options.path); } return args; -} - -export declare function transformReply(): Array | null | Array | null>; + }, + transformReply: undefined as unknown as () => ArrayReply | ArrayReply | NullReply> +} as const satisfies Command; diff --git a/packages/json/lib/commands/OBJLEN.spec.ts b/packages/json/lib/commands/OBJLEN.spec.ts index 35b6589c875..8f616107fd5 100644 --- a/packages/json/lib/commands/OBJLEN.spec.ts +++ b/packages/json/lib/commands/OBJLEN.spec.ts @@ -1,28 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './OBJLEN'; +import OBJLEN from './OBJLEN'; -describe('OBJLEN', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.OBJLEN', 'key'] - ); - }); +describe('JSON.OBJLEN', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + OBJLEN.transformArguments('key'), + ['JSON.OBJLEN', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.OBJLEN', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + OBJLEN.transformArguments('key', { + path: '$' + }), + ['JSON.OBJLEN', 'key', '$'] + ); }); + }); - // testUtils.testWithClient('client.json.objLen', async client => { - // assert.equal( - // await client.json.objLen('key', '$'), - // [null] - // ); - // }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.objLen', async client => { + assert.equal( + await client.json.objLen('key'), + null + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/OBJLEN.ts b/packages/json/lib/commands/OBJLEN.ts index aa800e97f71..f9c45e336ad 100644 --- a/packages/json/lib/commands/OBJLEN.ts +++ b/packages/json/lib/commands/OBJLEN.ts @@ -1,13 +1,20 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { +export interface JsonObjLenOptions { + path?: RedisArgument; +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, options?: JsonObjLenOptions) { const args = ['JSON.OBJLEN', key]; - if (path) { - args.push(path); + if (options?.path !== undefined) { + args.push(options.path); } return args; -} - -export declare function transformReply(): number | null | Array; + }, + transformReply: undefined as unknown as () => NumberReply | ArrayReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/RESP.spec.ts b/packages/json/lib/commands/RESP.spec.ts index 8b70962d1c5..6dfc3360326 100644 --- a/packages/json/lib/commands/RESP.spec.ts +++ b/packages/json/lib/commands/RESP.spec.ts @@ -1,4 +1,4 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import { transformArguments } from './RESP'; diff --git a/packages/json/lib/commands/SET.spec.ts b/packages/json/lib/commands/SET.spec.ts index 8f8586a2047..15e27073281 100644 --- a/packages/json/lib/commands/SET.spec.ts +++ b/packages/json/lib/commands/SET.spec.ts @@ -1,35 +1,35 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SET'; +import SET from './SET'; -describe('SET', () => { - describe('transformArguments', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '$', 'json'), - ['JSON.SET', 'key', '$', '"json"'] - ); - }); +describe('JSON.SET', () => { + describe('transformArguments', () => { + it('transformArguments', () => { + assert.deepEqual( + SET.transformArguments('key', '$', 'json'), + ['JSON.SET', 'key', '$', '"json"'] + ); + }); - it('NX', () => { - assert.deepEqual( - transformArguments('key', '$', 'json', { NX: true }), - ['JSON.SET', 'key', '$', '"json"', 'NX'] - ); - }); + it('NX', () => { + assert.deepEqual( + SET.transformArguments('key', '$', 'json', { NX: true }), + ['JSON.SET', 'key', '$', '"json"', 'NX'] + ); + }); - it('XX', () => { - assert.deepEqual( - transformArguments('key', '$', 'json', { XX: true }), - ['JSON.SET', 'key', '$', '"json"', 'XX'] - ); - }); + it('XX', () => { + assert.deepEqual( + SET.transformArguments('key', '$', 'json', { XX: true }), + ['JSON.SET', 'key', '$', '"json"', 'XX'] + ); }); + }); - testUtils.testWithClient('client.json.mGet', async client => { - assert.equal( - await client.json.set('key', '$', 'json'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.set', async client => { + assert.equal( + await client.json.set('key', '$', 'json'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/SET.ts b/packages/json/lib/commands/SET.ts index f50a42bf5db..78aea4b3545 100644 --- a/packages/json/lib/commands/SET.ts +++ b/packages/json/lib/commands/SET.ts @@ -1,25 +1,38 @@ +import { RedisArgument, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; -export const FIRST_KEY_INDEX = 1; - -interface NX { - NX: true; -} - -interface XX { - XX: true; +export interface JsonSetOptions { + condition?: 'NX' | 'XX'; + /** + * @deprecated Use `{ condition: 'NX' }` instead. + */ + NX?: boolean; + /** + * @deprecated Use `{ condition: 'XX' }` instead. + */ + XX?: boolean; } -export function transformArguments(key: string, path: string, json: RedisJSON, options?: NX | XX): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + path: RedisArgument, + json: RedisJSON, + options?: JsonSetOptions + ) { const args = ['JSON.SET', key, path, transformRedisJsonArgument(json)]; - if ((options)?.NX) { - args.push('NX'); - } else if ((options)?.XX) { - args.push('XX'); + if (options?.condition) { + args.push(options?.condition); + } else if (options?.NX) { + args.push('NX'); + } else if (options?.XX) { + args.push('XX'); } return args; -} - -export declare function transformReply(): 'OK' | null; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | NullReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/STRAPPEND.spec.ts b/packages/json/lib/commands/STRAPPEND.spec.ts index a37eaa1d91c..0d8bdb57185 100644 --- a/packages/json/lib/commands/STRAPPEND.spec.ts +++ b/packages/json/lib/commands/STRAPPEND.spec.ts @@ -1,30 +1,32 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './STRAPPEND'; +import STRAPPEND from './STRAPPEND'; -describe('STRAPPEND', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key', 'append'), - ['JSON.STRAPPEND', 'key', '"append"'] - ); - }); +describe('JSON.STRAPPEND', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + STRAPPEND.transformArguments('key', 'append'), + ['JSON.STRAPPEND', 'key', '"append"'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$', 'append'), - ['JSON.STRAPPEND', 'key', '$', '"append"'] - ); - }); + it('with path', () => { + assert.deepEqual( + STRAPPEND.transformArguments('key', 'append', { + path: '$' + }), + ['JSON.STRAPPEND', 'key', '$', '"append"'] + ); }); + }); - testUtils.testWithClient('client.json.strAppend', async client => { - await client.json.set('key', '$', ''); + testUtils.testWithClient('client.json.strAppend', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', ''), + client.json.strAppend('key', 'append') + ]); - assert.deepEqual( - await client.json.strAppend('key', '$', 'append'), - [6] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, 6); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/STRAPPEND.ts b/packages/json/lib/commands/STRAPPEND.ts index eea384c93fd..12ee7cc394c 100644 --- a/packages/json/lib/commands/STRAPPEND.ts +++ b/packages/json/lib/commands/STRAPPEND.ts @@ -1,21 +1,22 @@ +import { RedisArgument, Command, NullReply, NumberReply, ArrayReply } from '@redis/client/dist/lib/RESP/types'; import { transformRedisJsonArgument } from '.'; -export const FIRST_KEY_INDEX = 1; - -type AppendArguments = [key: string, append: string]; - -type AppendWithPathArguments = [key: string, path: string, append: string]; +export interface JsonStrAppendOptions { + path?: RedisArgument; +} -export function transformArguments(...[key, pathOrAppend, append]: AppendArguments | AppendWithPathArguments): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, append: string, options?: JsonStrAppendOptions) { const args = ['JSON.STRAPPEND', key]; - if (append !== undefined && append !== null) { - args.push(pathOrAppend, transformRedisJsonArgument(append)); - } else { - args.push(transformRedisJsonArgument(pathOrAppend)); + if (options?.path !== undefined) { + args.push(options.path); } + args.push(transformRedisJsonArgument(append)); return args; -} - -export declare function transformReply(): number | Array; + }, + transformReply: undefined as unknown as () => NumberReply | ArrayReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/STRLEN.spec.ts b/packages/json/lib/commands/STRLEN.spec.ts index cf163d3c19e..e058e48a635 100644 --- a/packages/json/lib/commands/STRLEN.spec.ts +++ b/packages/json/lib/commands/STRLEN.spec.ts @@ -1,30 +1,32 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './STRLEN'; +import STRLEN from './STRLEN'; -describe('STRLEN', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.STRLEN', 'key'] - ); - }); +describe('JSON.STRLEN', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + STRLEN.transformArguments('key'), + ['JSON.STRLEN', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.STRLEN', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + STRLEN.transformArguments('key', { + path: '$' + }), + ['JSON.STRLEN', 'key', '$'] + ); }); + }); - testUtils.testWithClient('client.json.strLen', async client => { - await client.json.set('key', '$', ''); + testUtils.testWithClient('client.json.strLen', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', ''), + client.json.strLen('key') + ]); - assert.deepEqual( - await client.json.strLen('key', '$'), - [0] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, 0); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/STRLEN.ts b/packages/json/lib/commands/STRLEN.ts index 93f5d563baf..3b514d9caba 100644 --- a/packages/json/lib/commands/STRLEN.ts +++ b/packages/json/lib/commands/STRLEN.ts @@ -1,15 +1,20 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const IS_READ_ONLY = true; +export interface JsonStrLenOptions { + path?: RedisArgument; +} -export function transformArguments(key: string, path?: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, options?: JsonStrLenOptions) { const args = ['JSON.STRLEN', key]; - if (path) { - args.push(path); + if (options?.path) { + args.push(options.path); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply | ArrayReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/TOGGLE.spec.ts b/packages/json/lib/commands/TOGGLE.spec.ts new file mode 100644 index 00000000000..c8a78877906 --- /dev/null +++ b/packages/json/lib/commands/TOGGLE.spec.ts @@ -0,0 +1,21 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import TOGGLE from './TOGGLE'; + +describe('JSON.TOGGLE', () => { + it('transformArguments', () => { + assert.deepEqual( + TOGGLE.transformArguments('key', '$'), + ['JSON.TOGGLE', 'key', '$'] + ); + }); + + testUtils.testWithClient('client.json.toggle', async client => { + const [, reply] = await Promise.all([ + client.json.set('key', '$', true), + client.json.toggle('key', '$') + ]); + + assert.deepEqual(reply, [0]); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/json/lib/commands/TOGGLE.ts b/packages/json/lib/commands/TOGGLE.ts new file mode 100644 index 00000000000..2a8df3eba36 --- /dev/null +++ b/packages/json/lib/commands/TOGGLE.ts @@ -0,0 +1,10 @@ +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command, } from '@redis/client/dist/lib/RESP/types'; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, path: RedisArgument) { + return ['JSON.TOGGLE', key, path]; + }, + transformReply: undefined as unknown as () => NumberReply | NullReply | ArrayReply +} as const satisfies Command; diff --git a/packages/json/lib/commands/TYPE.spec.ts b/packages/json/lib/commands/TYPE.spec.ts index 5cecfb827a7..103ce59de6e 100644 --- a/packages/json/lib/commands/TYPE.spec.ts +++ b/packages/json/lib/commands/TYPE.spec.ts @@ -1,28 +1,30 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './TYPE'; +import TYPE from './TYPE'; -describe('TYPE', () => { - describe('transformArguments', () => { - it('without path', () => { - assert.deepEqual( - transformArguments('key'), - ['JSON.TYPE', 'key'] - ); - }); +describe('JSON.TYPE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + TYPE.transformArguments('key'), + ['JSON.TYPE', 'key'] + ); + }); - it('with path', () => { - assert.deepEqual( - transformArguments('key', '$'), - ['JSON.TYPE', 'key', '$'] - ); - }); + it('with path', () => { + assert.deepEqual( + TYPE.transformArguments('key', { + path: '$' + }), + ['JSON.TYPE', 'key', '$'] + ); }); + }); - // testUtils.testWithClient('client.json.type', async client => { - // assert.deepEqual( - // await client.json.type('key', '$'), - // [null] - // ); - // }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.json.type', async client => { + assert.equal( + await client.json.type('key'), + null + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/json/lib/commands/TYPE.ts b/packages/json/lib/commands/TYPE.ts index 7fd55f625dc..c2eea9856e1 100644 --- a/packages/json/lib/commands/TYPE.ts +++ b/packages/json/lib/commands/TYPE.ts @@ -1,13 +1,27 @@ -export const FIRST_KEY_INDEX = 1; +import { NullReply, BlobStringReply, ArrayReply, Command, RedisArgument, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string, path?: string): Array { +export interface JsonTypeOptions { + path?: RedisArgument; +} + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, options?: JsonTypeOptions) { const args = ['JSON.TYPE', key]; - if (path) { - args.push(path); + if (options?.path) { + args.push(options.path); } return args; -} + }, + transformReply: { + 2: undefined as unknown as () => NullReply | BlobStringReply | ArrayReply, + // TODO: RESP3 wraps the response in another array, but only returns 1 + 3: (reply: UnwrapReply>>) => { + return reply[0]; + } + }, +} as const satisfies Command; -export declare function transformReply(): string | null | Array; diff --git a/packages/json/lib/commands/index.ts b/packages/json/lib/commands/index.ts index 9d0a82ec271..2724ff2565c 100644 --- a/packages/json/lib/commands/index.ts +++ b/packages/json/lib/commands/index.ts @@ -1,96 +1,100 @@ -import * as ARRAPPEND from './ARRAPPEND'; -import * as ARRINDEX from './ARRINDEX'; -import * as ARRINSERT from './ARRINSERT'; -import * as ARRLEN from './ARRLEN'; -import * as ARRPOP from './ARRPOP'; -import * as ARRTRIM from './ARRTRIM'; -import * as DEBUG_MEMORY from './DEBUG_MEMORY'; -import * as DEL from './DEL'; -import * as FORGET from './FORGET'; -import * as GET from './GET'; -import * as MERGE from './MERGE'; -import * as MGET from './MGET'; -import * as MSET from './MSET'; -import * as NUMINCRBY from './NUMINCRBY'; -import * as NUMMULTBY from './NUMMULTBY'; -import * as OBJKEYS from './OBJKEYS'; -import * as OBJLEN from './OBJLEN'; -import * as RESP from './RESP'; -import * as SET from './SET'; -import * as STRAPPEND from './STRAPPEND'; -import * as STRLEN from './STRLEN'; -import * as TYPE from './TYPE'; +import { BlobStringReply, NullReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import ARRAPPEND from './ARRAPPEND'; +import ARRINDEX from './ARRINDEX'; +import ARRINSERT from './ARRINSERT'; +import ARRLEN from './ARRLEN'; +import ARRPOP from './ARRPOP'; +import ARRTRIM from './ARRTRIM'; +import CLEAR from './CLEAR'; +import DEBUG_MEMORY from './DEBUG_MEMORY'; +import DEL from './DEL'; +import FORGET from './FORGET'; +import GET from './GET'; +import MERGE from './MERGE'; +import MGET from './MGET'; +import MSET from './MSET'; +import NUMINCRBY from './NUMINCRBY'; +import NUMMULTBY from './NUMMULTBY'; +import OBJKEYS from './OBJKEYS'; +import OBJLEN from './OBJLEN'; +// import RESP from './RESP'; +import SET from './SET'; +import STRAPPEND from './STRAPPEND'; +import STRLEN from './STRLEN'; +import TOGGLE from './TOGGLE'; +import TYPE from './TYPE'; +import { isNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { - ARRAPPEND, - arrAppend: ARRAPPEND, - ARRINDEX, - arrIndex: ARRINDEX, - ARRINSERT, - arrInsert: ARRINSERT, - ARRLEN, - arrLen: ARRLEN, - ARRPOP, - arrPop: ARRPOP, - ARRTRIM, - arrTrim: ARRTRIM, - DEBUG_MEMORY, - debugMemory: DEBUG_MEMORY, - DEL, - del: DEL, - FORGET, - forget: FORGET, - GET, - get: GET, - MERGE, - merge: MERGE, - MGET, - mGet: MGET, - MSET, - mSet: MSET, - NUMINCRBY, - numIncrBy: NUMINCRBY, - NUMMULTBY, - numMultBy: NUMMULTBY, - OBJKEYS, - objKeys: OBJKEYS, - OBJLEN, - objLen: OBJLEN, - RESP, - resp: RESP, - SET, - set: SET, - STRAPPEND, - strAppend: STRAPPEND, - STRLEN, - strLen: STRLEN, - TYPE, - type: TYPE + ARRAPPEND, + arrAppend: ARRAPPEND, + ARRINDEX, + arrIndex: ARRINDEX, + ARRINSERT, + arrInsert: ARRINSERT, + ARRLEN, + arrLen: ARRLEN, + ARRPOP, + arrPop: ARRPOP, + ARRTRIM, + arrTrim: ARRTRIM, + CLEAR, + clear: CLEAR, + DEBUG_MEMORY, + debugMemory: DEBUG_MEMORY, + DEL, + del: DEL, + FORGET, + forget: FORGET, + GET, + get: GET, + MERGE, + merge: MERGE, + MGET, + mGet: MGET, + MSET, + mSet: MSET, + NUMINCRBY, + numIncrBy: NUMINCRBY, + /** + * @deprecated since JSON version 2.0 + */ + NUMMULTBY, + /** + * @deprecated since JSON version 2.0 + */ + numMultBy: NUMMULTBY, + OBJKEYS, + objKeys: OBJKEYS, + OBJLEN, + objLen: OBJLEN, + // RESP, + // resp: RESP, + SET, + set: SET, + STRAPPEND, + strAppend: STRAPPEND, + STRLEN, + strLen: STRLEN, + TOGGLE, + toggle: TOGGLE, + TYPE, + type: TYPE }; -// https://github.com/Microsoft/TypeScript/issues/3496#issuecomment-128553540 -// eslint-disable-next-line @typescript-eslint/no-empty-interface -interface RedisJSONArray extends Array {} -interface RedisJSONObject { - [key: string]: RedisJSON; - [key: number]: RedisJSON; -} -export type RedisJSON = null | boolean | number | string | Date | RedisJSONArray | RedisJSONObject; +export type RedisJSON = null | boolean | number | string | Date | Array | { + [key: string]: RedisJSON; + [key: number]: RedisJSON; +}; export function transformRedisJsonArgument(json: RedisJSON): string { - return JSON.stringify(json); + return JSON.stringify(json); } -export function transformRedisJsonReply(json: string): RedisJSON { - return JSON.parse(json); -} - -export function transformRedisJsonNullReply(json: string | null): RedisJSON | null { - if (json === null) return null; - - return transformRedisJsonReply(json); +export function transformRedisJsonReply(json: BlobStringReply): RedisJSON { + return JSON.parse((json as unknown as UnwrapReply).toString()); } -export function transformNumbersReply(reply: string): number | Array { - return JSON.parse(reply); +export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON { + return isNullReply(json) ? json : transformRedisJsonReply(json); } diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index 55426890e00..0ac30c521b2 100644 --- a/packages/json/lib/test-utils.ts +++ b/packages/json/lib/test-utils.ts @@ -2,20 +2,20 @@ import TestUtils from '@redis/test-utils'; import RedisJSON from '.'; export default new TestUtils({ - dockerImageName: 'redislabs/rejson', - dockerImageVersionArgument: 'rejson-version', - defaultDockerVersion: '2.6.9' + dockerImageName: 'redis/redis-stack', + dockerImageVersionArgument: 'redisgraph-version', + defaultDockerVersion: '7.4.0-v1' }); export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: ['--loadmodule /usr/lib/redis/modules/rejson.so'], - clientOptions: { - modules: { - json: RedisJSON - } - } + SERVERS: { + OPEN: { + serverArguments: [], + clientOptions: { + modules: { + json: RedisJSON } + } } + } }; diff --git a/packages/json/package.json b/packages/json/package.json index ad60cc13c26..53711a5c0b6 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,30 +1,24 @@ { "name": "@redis/json", - "version": "1.0.7", + "version": "2.0.0-next.2", "license": "MIT", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/lib/index.js", + "types": "./dist/lib/index.d.ts", "files": [ - "dist/" + "dist/", + "!dist/tsconfig.tsbuildinfo" ], "scripts": { - "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'", - "build": "tsc", - "documentation": "typedoc" + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" }, "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "repository": { "type": "git", diff --git a/.release-it.json b/packages/redis/.release-it.json similarity index 100% rename from .release-it.json rename to packages/redis/.release-it.json diff --git a/packages/redis/README.md b/packages/redis/README.md new file mode 100644 index 00000000000..76929ffa48b --- /dev/null +++ b/packages/redis/README.md @@ -0,0 +1,203 @@ +# Node-Redis + +## Usage + +### Basic Example + +```javascript +import { createClient } from 'redis'; + +const client = await createClient() + .on('error', err => console.log('Redis Client Error', err)) + .connect(); + +await client.set('key', 'value'); +const value = await client.get('key'); +await client.close(); +``` + +> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and an `error` occurs, that error will be thrown and the Node.js process will exit. See the [`EventEmitter` docs](https://nodejs.org/api/events.html#error-events) for more details. + +The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in the format `redis[s]://[[username][:password]@][host][:port][/db-number]`: + +```javascript +createClient({ + url: 'redis://alice:foobared@awesome.redis.server:6380' +}); +``` + +You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in the [client configuration guide](../../docs/client-configuration.md). + +### Redis Commands + +There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`, etc.): + +```javascript +// raw Redis commands +await client.HSET('key', 'field', 'value'); +await client.HGETALL('key'); + +// friendly JavaScript commands +await client.hSet('key', 'field', 'value'); +await client.hGetAll('key'); +``` + +Modifiers to commands are specified using a JavaScript object: + +```javascript +await client.set('key', 'value', { + expiration: { + type: 'EX', + value: 10 + }, + condition: 'NX' +}); +``` + +> NOTE: command modifiers that change the reply type (e.g. `WITHSCORES` for `ZDIFF`) are exposed as separate commands (e.g. `ZDIFF_WITHSCORES`/`zDiffWithScores`). + +Replies will be mapped to useful data structures: + +```javascript +await client.hGetAll('key'); // { field1: 'value1', field2: 'value2' } +await client.hVals('key'); // ['value1', 'value2'] +``` + +> NOTE: you can change the default type mapping. See the [Type Mapping](../../docs/command-options.md#type-mapping) documentation for more information. + +### Unsupported Redis Commands + +If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`: + +```javascript +await client.sendCommand(['SET', 'key', 'value', 'EX', '10', 'NX']); // 'OK' +await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2'] +``` + +### Disconnecting + +#### `.close()` + +Gracefully close a client's connection to Redis. +Wait for commands in process, but reject any new commands. + +```javascript +const [ping, get] = await Promise.all([ + client.ping(), + client.get('key'), + client.close() +]); // ['PONG', null] + +try { + await client.get('key'); +} catch (err) { + // ClientClosedError +} +``` + +> `.close()` is just like `.quit()` which was depreacted v5. See the [relevant section in the migration guide](../../docs/v4-to-v5.md#Quit-VS-Disconnect) for more information. + +#### `.destroy()` + +Forcibly close a client's connection to Redis. + +```javascript +try { + const promise = Promise.all([ + client.ping(), + client.get('key') + ]); + + client.destroy(); + + await promise; +} catch (err) { + // DisconnectsClientError +} + +try { + await client.get('key'); +} catch (err) { + // ClientClosedError +} +``` + +> `.destroy()` is just like `.disconnect()` which was depreated in v5. See the [relevant section in the migration guide](../../docs/v4-to-v5.md#Quit-VS-Disconnect) for more information. + +### Auto-Pipelining + +Node Redis will automatically pipeline requests that are made during the same "tick". + +```javascript +client.set('Tm9kZSBSZWRpcw==', 'users:1'); +client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw=='); +``` + +Of course, if you don't do something with your Promises you're certain to get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take advantage of auto-pipelining and handle your Promises, use `Promise.all()`. + +```javascript +await Promise.all([ + client.set('Tm9kZSBSZWRpcw==', 'users:1'), + client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==') +]); +``` + +### Connection State + +To client exposes 2 `boolean`s that track the client state: +1. `isOpen` - the client is either connecting or connected. +2. `isReady` - the client is connected and ready to send + +### Events + +The client extends `EventEmitter` and emits the following events: + +| Name | When | Listener arguments | +|-------------------------|------------------------------------------------------------------------------------|------------------------------------------------------------| +| `connect` | Initiating a connection to the server | *No arguments* | +| `ready` | Client is ready to use | *No arguments* | +| `end` | Connection has been closed (via `.quit()` or `.disconnect()`) | *No arguments* | +| `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` | +| `reconnecting` | Client is trying to reconnect to the server | *No arguments* | +| `sharded-channel-moved` | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | + +> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and an `error` occurs, that error will be thrown and the Node.js process will exit. See the [`EventEmitter` docs](https://nodejs.org/api/events.html#error-events) for more details. + +### Read more + +- [Transactions (`MULTI`/`EXEC`)](../../docs/transactions.md). +- [Pub/Sub](../../docs/pub-sub.md). +- [Scan Iterators](../../docs/scan-iterators.md). +- [Programmability](../../docs/programmability.md). +- [Command Options](../../docs/command-options.md). +- [Pool](../../docs/pool.md). +- [Clustering](../../docs/clustering.md). +- [Sentinel](../../docs/sentinel.md). +- [FAQ](../../docs/FAQ.md). + +## Supported Redis versions + +Node Redis is supported with the following versions of Redis: + +| Version | Supported | +|---------|--------------------| +| 7.2.z | :heavy_check_mark: | +| 7.0.z | :heavy_check_mark: | +| 6.2.z | :heavy_check_mark: | +| 6.0.z | :heavy_check_mark: | +| 5.0.z | :heavy_check_mark: | +| < 5.0 | :x: | + +> Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support. + +## Contributing + +If you'd like to contribute, check out the [contributing guide](../../CONTRIBUTING.md). + +Thank you to all the people who already contributed to Node Redis! + +[![Contributors](https://contrib.rocks/image?repo=redis/node-redis)](https://github.com/redis/node-redis/graphs/contributors) + +## License + +This repository is licensed under the "MIT" license. See [LICENSE](../../LICENSE). diff --git a/packages/redis/index.ts b/packages/redis/index.ts new file mode 100644 index 00000000000..7586846d12c --- /dev/null +++ b/packages/redis/index.ts @@ -0,0 +1,87 @@ +import { + RedisModules, + RedisFunctions, + RedisScripts, + RespVersions, + TypeMapping, + createClient as _createClient, + RedisClientOptions, + RedisClientType as _RedisClientType, + createCluster as _createCluster, + RedisClusterOptions, + RedisClusterType as _RedisClusterType, +} from '@redis/client'; +import RedisBloomModules from '@redis/bloom'; +import RedisGraph from '@redis/graph'; +import RedisJSON from '@redis/json'; +import RediSearch from '@redis/search'; +import RedisTimeSeries from '@redis/time-series'; + +// export * from '@redis/client'; +// export * from '@redis/bloom'; +// export * from '@redis/graph'; +// export * from '@redis/json'; +// export * from '@redis/search'; +// export * from '@redis/time-series'; + +const modules = { + ...RedisBloomModules, + graph: RedisGraph, + json: RedisJSON, + ft: RediSearch, + ts: RedisTimeSeries +}; + +export type RedisDefaultModules = typeof modules; + +export type RedisClientType< + M extends RedisModules = RedisDefaultModules, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} +> = _RedisClientType; + +export function createClient< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +>( + options?: RedisClientOptions +): _RedisClientType { + return _createClient({ + ...options, + modules: { + ...modules, + ...(options?.modules as M) + } + }); +} + +export type RedisClusterType< + M extends RedisModules = RedisDefaultModules, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} +> = _RedisClusterType; + +export function createCluster< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +>( + options: RedisClusterOptions +): RedisClusterType { + return _createCluster({ + ...options, + modules: { + ...modules, + ...(options?.modules as M) + } + }); +} diff --git a/packages/redis/package.json b/packages/redis/package.json new file mode 100644 index 00000000000..0603e6c9480 --- /dev/null +++ b/packages/redis/package.json @@ -0,0 +1,34 @@ +{ + "name": "redis", + "description": "A modern, high performance Redis client", + "version": "5.0.0-next.4", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/", + "!dist/tsconfig.tsbuildinfo" + ], + "dependencies": { + "@redis/bloom": "2.0.0-next.3", + "@redis/client": "2.0.0-next.4", + "@redis/graph": "2.0.0-next.2", + "@redis/json": "2.0.0-next.2", + "@redis/search": "2.0.0-next.2", + "@redis/time-series": "2.0.0-next.2" + }, + "engines": { + "node": ">= 18" + }, + "repository": { + "type": "git", + "url": "git://github.com/redis/node-redis.git" + }, + "bugs": { + "url": "https://github.com/redis/node-redis/issues" + }, + "homepage": "https://github.com/redis/node-redis", + "keywords": [ + "redis" + ] +} diff --git a/packages/redis/tsconfig.json b/packages/redis/tsconfig.json new file mode 100644 index 00000000000..50da0ba733a --- /dev/null +++ b/packages/redis/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "./index.ts" + ] +} diff --git a/packages/search/.npmignore b/packages/search/.npmignore deleted file mode 100644 index bbef2b404fb..00000000000 --- a/packages/search/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -.nyc_output/ -coverage/ -lib/ -.nycrc.json -.release-it.json -tsconfig.json diff --git a/packages/search/.release-it.json b/packages/search/.release-it.json index 72cb1016ef4..3996a524e3b 100644 --- a/packages/search/.release-it.json +++ b/packages/search/.release-it.json @@ -5,6 +5,7 @@ "tagAnnotation": "Release ${tagName}" }, "npm": { + "versionArgs": ["--workspaces-update=false"], "publishArgs": ["--access", "public"] } } diff --git a/packages/search/README.md b/packages/search/README.md index 60186ba7f92..70a91fdeb2f 100644 --- a/packages/search/README.md +++ b/packages/search/README.md @@ -1,18 +1,20 @@ # @redis/search -This package provides support for the [RediSearch](https://redisearch.io) module, which adds indexing and querying support for data stored in Redis Hashes or as JSON documents with the RedisJSON module. It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RediSearch commands. +This package provides support for the [RediSearch](https://redis.io/docs/interact/search-and-query/) module, which adds indexing and querying support for data stored in Redis Hashes or as JSON documents with the [RedisJSON](https://redis.io/docs/data-types/json/) module. -To use these extra commands, your Redis server must have the RediSearch module installed. To index and query JSON documents, you'll also need to add the RedisJSON module. +Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis). + +:warning: To use these extra commands, your Redis server must have the RediSearch module installed. To index and query JSON documents, you'll also need to add the RedisJSON module. ## Usage -For complete examples, see [`search-hashes.js`](https://github.com/redis/node-redis/blob/master/examples/search-hashes.js) and [`search-json.js`](https://github.com/redis/node-redis/blob/master/examples/search-json.js) in the Node Redis examples folder. +For complete examples, see [`search-hashes.js`](https://github.com/redis/node-redis/blob/master/examples/search-hashes.js) and [`search-json.js`](https://github.com/redis/node-redis/blob/master/examples/search-json.js) in the [examples folder](https://github.com/redis/node-redis/tree/master/examples). ### Indexing and Querying Data in Redis Hashes #### Creating an Index -Before we can perform any searches, we need to tell RediSearch how to index our data, and which Redis keys to find that data in. The [FT.CREATE](https://redis.io/commands/ft.create) command creates a RediSearch index. Here's how to use it to create an index we'll call `idx:animals` where we want to index hashes containing `name`, `species` and `age` fields, and whose key names in Redis begin with the prefix `noderedis:animals`: +Before we can perform any searches, we need to tell RediSearch how to index our data, and which Redis keys to find that data in. The [FT.CREATE](https://redis.io/commands/ft.create) command creates a RediSearch index. Here's how to use it to create an index we'll call `idx:animals` where we want to index hashes containing `name`, `species` and `age` fields, and whose key names in Redis begin with the prefix `noderedis:animals`: ```javascript await client.ft.create('idx:animals', { diff --git a/packages/search/lib/commands/AGGREGATE.spec.ts b/packages/search/lib/commands/AGGREGATE.spec.ts index 5b34d7dc16f..50ef44f2bd6 100644 --- a/packages/search/lib/commands/AGGREGATE.spec.ts +++ b/packages/search/lib/commands/AGGREGATE.spec.ts @@ -1,516 +1,521 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { AggregateGroupByReducers, AggregateSteps, transformArguments } from './AGGREGATE'; -import { SchemaFieldTypes } from '.'; +import AGGREGATE from './AGGREGATE'; describe('AGGREGATE', () => { - describe('transformArguments', () => { - it('without options', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*'), + ['FT.AGGREGATE', 'index', '*'] + ); + }); + + it('with VERBATIM', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + VERBATIM: true + }), + ['FT.AGGREGATE', 'index', '*', 'VERBATIM'] + ); + }); + + it('with ADDSCORES', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { ADDSCORES: true }), + ['FT.AGGREGATE', 'index', '*', 'ADDSCORES'] + ); + }); + + describe('with LOAD', () => { + describe('single', () => { + describe('without alias', () => { + it('string', () => { assert.deepEqual( - transformArguments('index', '*'), - ['FT.AGGREGATE', 'index', '*'] + AGGREGATE.transformArguments('index', '*', { + LOAD: '@property' + }), + ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property'] ); - }); + }); - it('with VERBATIM', () => { + it('{ identifier: string }', () => { assert.deepEqual( - transformArguments('index', '*', { VERBATIM: true }), - ['FT.AGGREGATE', 'index', '*', 'VERBATIM'] + AGGREGATE.transformArguments('index', '*', { + LOAD: { + identifier: '@property' + } + }), + ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property'] ); + }); }); - it('with ADDSCORES', () => { - assert.deepEqual( - transformArguments('index', '*', { ADDSCORES: true }), - ['FT.AGGREGATE', 'index', '*', 'ADDSCORES'] - ); + it('with alias', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + LOAD: { + identifier: '@property', + AS: 'alias' + } + }), + ['FT.AGGREGATE', 'index', '*', 'LOAD', '3', '@property', 'AS', 'alias'] + ); }); + }); + + it('multiple', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + LOAD: ['@1', '@2'] + }), + ['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2'] + ); + }); + }); - describe('with LOAD', () => { - describe('single', () => { - describe('without alias', () => { - it('string', () => { - assert.deepEqual( - transformArguments('index', '*', { LOAD: '@property' }), - ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property'] - ); - }); - - it('{ identifier: string }', () => { - assert.deepEqual( - transformArguments('index', '*', { - LOAD: { - identifier: '@property' - } - }), - ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property'] - ); - }); - }); - - it('with alias', () => { - assert.deepEqual( - transformArguments('index', '*', { - LOAD: { - identifier: '@property', - AS: 'alias' - } - }), - ['FT.AGGREGATE', 'index', '*', 'LOAD', '3', '@property', 'AS', 'alias'] - ); - }); + describe('with STEPS', () => { + describe('GROUPBY', () => { + describe('COUNT', () => { + describe('without properties', () => { + it('without alias', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'COUNT' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0'] + ); + }); + + it('with alias', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'COUNT', + AS: 'count' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'AS', 'count'] + ); + }); + }); + + describe('with properties', () => { + it('single', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + properties: '@property', + REDUCE: { + type: 'COUNT' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '1', '@property', 'REDUCE', 'COUNT', '0'] + ); }); it('multiple', () => { - assert.deepEqual( - transformArguments('index', '*', { LOAD: ['@1', '@2'] }), - ['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2'] - ); + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + properties: ['@1', '@2'], + REDUCE: { + type: 'COUNT' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '2', '@1', '@2', 'REDUCE', 'COUNT', '0'] + ); }); + }); }); - describe('with STEPS', () => { - describe('GROUPBY', () => { - describe('COUNT', () => { - describe('without properties', () => { - it('without alias', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.COUNT - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0'] - ); - }); - - it('with alias', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.COUNT, - AS: 'count' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'AS', 'count'] - ); - }); - }); - - describe('with properties', () => { - it('single', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - properties: '@property', - REDUCE: { - type: AggregateGroupByReducers.COUNT - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '1', '@property', 'REDUCE', 'COUNT', '0'] - ); - }); - - it('multiple', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - properties: ['@1', '@2'], - REDUCE: { - type: AggregateGroupByReducers.COUNT - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '2', '@1', '@2', 'REDUCE', 'COUNT', '0'] - ); - }); - }); - }); - - it('COUNT_DISTINCT', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.COUNT_DISTINCT, - property: '@property' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCT', '1', '@property'] - ); - }); - - it('COUNT_DISTINCTISH', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.COUNT_DISTINCTISH, - property: '@property' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@property'] - ); - }); - - it('SUM', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.SUM, - property: '@property' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'SUM', '1', '@property'] - ); - }); - - it('MIN', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.MIN, - property: '@property' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MIN', '1', '@property'] - ); - }); - - it('MAX', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.MAX, - property: '@property' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MAX', '1', '@property'] - ); - }); - - it('AVG', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.AVG, - property: '@property' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'AVG', '1', '@property'] - ); - }); - - it('STDDEV', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.STDDEV, - property: '@property' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'STDDEV', '1', '@property'] - ); - }); - - it('QUANTILE', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.QUANTILE, - property: '@property', - quantile: 0.5 - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'QUANTILE', '2', '@property', '0.5'] - ); - }); - - it('TO_LIST', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.TO_LIST, - property: '@property' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'TOLIST', '1', '@property'] - ); - }); - - describe('FIRST_VALUE', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.FIRST_VALUE, - property: '@property' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '1', '@property'] - ); - }); - - describe('with BY', () => { - describe('without direction', () => { - it('string', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.FIRST_VALUE, - property: '@property', - BY: '@by' - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by'] - ); - }); - - - it('{ property: string }', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.FIRST_VALUE, - property: '@property', - BY: { - property: '@by' - } - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by'] - ); - }); - }); - - it('with direction', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.FIRST_VALUE, - property: '@property', - BY: { - property: '@by', - direction: 'ASC' - } - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '4', '@property', 'BY', '@by', 'ASC'] - ); - }); - }); - }); - - it('RANDOM_SAMPLE', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: { - type: AggregateGroupByReducers.RANDOM_SAMPLE, - property: '@property', - sampleSize: 1 - } - }] - }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'RANDOM_SAMPLE', '2', '@property', '1'] - ); - }); - }); + it('COUNT_DISTINCT', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'COUNT_DISTINCT', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCT', '1', '@property'] + ); + }); - describe('SORTBY', () => { - it('string', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.SORTBY, - BY: '@by' - }] - }), - ['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by'] - ); - }); - - it('Array', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.SORTBY, - BY: ['@1', '@2'] - }] - }), - ['FT.AGGREGATE', 'index', '*', 'SORTBY', '2', '@1', '@2'] - ); - }); - - it('with MAX', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.SORTBY, - BY: '@by', - MAX: 1 - }] - }), - ['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by', 'MAX', '1'] - ); - }); - }); + it('COUNT_DISTINCTISH', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'COUNT_DISTINCTISH', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@property'] + ); + }); - describe('APPLY', () => { - assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.APPLY, - expression: '@field + 1', - AS: 'as' - }] - }), - ['FT.AGGREGATE', 'index', '*', 'APPLY', '@field + 1', 'AS', 'as'] - ); - }); + it('SUM', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'SUM', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'SUM', '1', '@property'] + ); + }); - describe('LIMIT', () => { + it('MIN', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'MIN', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MIN', '1', '@property'] + ); + }); + + it('MAX', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'MAX', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MAX', '1', '@property'] + ); + }); + + it('AVG', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'AVG', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'AVG', '1', '@property'] + ); + }); + + it('STDDEV', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'STDDEV', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'STDDEV', '1', '@property'] + ); + }); + + it('QUANTILE', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'QUANTILE', + property: '@property', + quantile: 0.5 + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'QUANTILE', '2', '@property', '0.5'] + ); + }); + + it('TOLIST', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'TOLIST', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'TOLIST', '1', '@property'] + ); + }); + + describe('FIRST_VALUE', () => { + it('simple', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'FIRST_VALUE', + property: '@property' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '1', '@property'] + ); + }); + + describe('with BY', () => { + describe('without direction', () => { + it('string', () => { assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.LIMIT, - from: 0, - size: 1 - }] - }), - ['FT.AGGREGATE', 'index', '*', 'LIMIT', '0', '1'] + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'FIRST_VALUE', + property: '@property', + BY: '@by' + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by'] ); - }); + }); + - describe('FILTER', () => { + it('{ property: string }', () => { assert.deepEqual( - transformArguments('index', '*', { - STEPS: [{ - type: AggregateSteps.FILTER, - expression: '@field != ""' - }] - }), - ['FT.AGGREGATE', 'index', '*', 'FILTER', '@field != ""'] + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'FIRST_VALUE', + property: '@property', + BY: { + property: '@by' + } + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by'] ); + }); }); - }); - it('with PARAMS', () => { - assert.deepEqual( - transformArguments('index', '*', { - PARAMS: { - param: 'value' + it('with direction', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'FIRST_VALUE', + property: '@property', + BY: { + property: '@by', + direction: 'ASC' + } } + }] }), - ['FT.AGGREGATE', 'index', '*', 'PARAMS', '2', 'param', 'value'] - ); + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '4', '@property', 'BY', '@by', 'ASC'] + ); + }); + }); }); - it('with DIALECT', () => { - assert.deepEqual( - transformArguments('index', '*', { - DIALECT: 1 - }), - ['FT.AGGREGATE', 'index', '*', 'DIALECT', '1'] - ); + it('RANDOM_SAMPLE', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: { + type: 'RANDOM_SAMPLE', + property: '@property', + sampleSize: 1 + } + }] + }), + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'RANDOM_SAMPLE', '2', '@property', '1'] + ); + }); + }); + + describe('SORTBY', () => { + it('string', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'SORTBY', + BY: '@by' + }] + }), + ['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by'] + ); }); - it('with TIMEOUT', () => { - assert.deepEqual( - transformArguments('index', '*', { TIMEOUT: 10 }), - ['FT.AGGREGATE', 'index', '*', 'TIMEOUT', '10'] - ); + it('Array', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'SORTBY', + BY: ['@1', '@2'] + }] + }), + ['FT.AGGREGATE', 'index', '*', 'SORTBY', '2', '@1', '@2'] + ); }); - }); - testUtils.testWithClient('client.ft.aggregate', async client => { - await Promise.all([ - client.ft.create('index', { - field: SchemaFieldTypes.NUMERIC + it('with MAX', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'SORTBY', + BY: '@by', + MAX: 1 + }] }), - client.hSet('1', 'field', '1'), - client.hSet('2', 'field', '2') - ]); + ['FT.AGGREGATE', 'index', '*', 'SORTBY', '3', '@by', 'MAX', '1'] + ); + }); + }); + describe('APPLY', () => { assert.deepEqual( - await client.ft.aggregate('index', '*', { - STEPS: [{ - type: AggregateSteps.GROUPBY, - REDUCE: [{ - type: AggregateGroupByReducers.SUM, - property: '@field', - AS: 'sum' - }, { - type: AggregateGroupByReducers.AVG, - property: '@field', - AS: 'avg' - }] - }] - }), - { - total: 1, - results: [ - Object.create(null, { - sum: { - value: '3', - configurable: true, - enumerable: true - }, - avg: { - value: '1.5', - configurable: true, - enumerable: true - } - }) - ] - } + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'APPLY', + expression: '@field + 1', + AS: 'as' + }] + }), + ['FT.AGGREGATE', 'index', '*', 'APPLY', '@field + 1', 'AS', 'as'] + ); + }); + + describe('LIMIT', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'LIMIT', + from: 0, + size: 1 + }] + }), + ['FT.AGGREGATE', 'index', '*', 'LIMIT', '0', '1'] + ); + }); + + describe('FILTER', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + STEPS: [{ + type: 'FILTER', + expression: '@field != ""' + }] + }), + ['FT.AGGREGATE', 'index', '*', 'FILTER', '@field != ""'] ); - }, GLOBAL.SERVERS.OPEN); + }); + }); + + it('with PARAMS', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + PARAMS: { + param: 'value' + } + }), + ['FT.AGGREGATE', 'index', '*', 'PARAMS', '2', 'param', 'value'] + ); + }); + + it('with DIALECT', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { + DIALECT: 1 + }), + ['FT.AGGREGATE', 'index', '*', 'DIALECT', '1'] + ); + }); + + it('with TIMEOUT', () => { + assert.deepEqual( + AGGREGATE.transformArguments('index', '*', { TIMEOUT: 10 }), + ['FT.AGGREGATE', 'index', '*', 'TIMEOUT', '10'] + ); + }); + }); + + testUtils.testWithClient('client.ft.aggregate', async client => { + await Promise.all([ + client.ft.create('index', { + field: 'NUMERIC' + }), + client.hSet('1', 'field', '1'), + client.hSet('2', 'field', '2') + ]); + + assert.deepEqual( + await client.ft.aggregate('index', '*', { + STEPS: [{ + type: 'GROUPBY', + REDUCE: [{ + type: 'SUM', + property: '@field', + AS: 'sum' + }, { + type: 'AVG', + property: '@field', + AS: 'avg' + }] + }] + }), + { + total: 1, + results: [ + Object.create(null, { + sum: { + value: '3', + configurable: true, + enumerable: true + }, + avg: { + value: '1.5', + configurable: true, + enumerable: true + } + }) + ] + } + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts index 0cab9b25d48..cb9652622ad 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -1,316 +1,329 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArgument, transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers'; -import { Params, PropertyName, pushArgumentsWithLength, pushParamsArgs, pushSortByArguments, SortByProperty } from '.'; - -export enum AggregateSteps { - GROUPBY = 'GROUPBY', - SORTBY = 'SORTBY', - APPLY = 'APPLY', - LIMIT = 'LIMIT', - FILTER = 'FILTER' +import { ArrayReply, BlobStringReply, Command, MapReply, NumberReply, RedisArgument, ReplyUnion, TypeMapping, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import { RediSearchProperty } from './CREATE'; +import { FtSearchParams, pushParamsArgument } from './SEARCH'; +import { pushVariadicArgument, transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers'; + +type LoadField = RediSearchProperty | { + identifier: RediSearchProperty; + AS?: RedisArgument; } -interface AggregateStep { - type: T; +export const FT_AGGREGATE_STEPS = { + GROUPBY: 'GROUPBY', + SORTBY: 'SORTBY', + APPLY: 'APPLY', + LIMIT: 'LIMIT', + FILTER: 'FILTER' +} as const; + +type FT_AGGREGATE_STEPS = typeof FT_AGGREGATE_STEPS; + +export type FtAggregateStep = FT_AGGREGATE_STEPS[keyof FT_AGGREGATE_STEPS]; + +interface AggregateStep { + type: T; } -export enum AggregateGroupByReducers { - COUNT = 'COUNT', - COUNT_DISTINCT = 'COUNT_DISTINCT', - COUNT_DISTINCTISH = 'COUNT_DISTINCTISH', - SUM = 'SUM', - MIN = 'MIN', - MAX = 'MAX', - AVG = 'AVG', - STDDEV = 'STDDEV', - QUANTILE = 'QUANTILE', - TOLIST = 'TOLIST', - TO_LIST = 'TOLIST', - FIRST_VALUE = 'FIRST_VALUE', - RANDOM_SAMPLE = 'RANDOM_SAMPLE' +export const FT_AGGREGATE_GROUP_BY_REDUCERS = { + COUNT: 'COUNT', + COUNT_DISTINCT: 'COUNT_DISTINCT', + COUNT_DISTINCTISH: 'COUNT_DISTINCTISH', + SUM: 'SUM', + MIN: 'MIN', + MAX: 'MAX', + AVG: 'AVG', + STDDEV: 'STDDEV', + QUANTILE: 'QUANTILE', + TOLIST: 'TOLIST', + FIRST_VALUE: 'FIRST_VALUE', + RANDOM_SAMPLE: 'RANDOM_SAMPLE' +} as const; + +type FT_AGGREGATE_GROUP_BY_REDUCERS = typeof FT_AGGREGATE_GROUP_BY_REDUCERS; + +export type FtAggregateGroupByReducer = FT_AGGREGATE_GROUP_BY_REDUCERS[keyof FT_AGGREGATE_GROUP_BY_REDUCERS]; + +interface GroupByReducer { + type: T; + AS?: RedisArgument; } -interface GroupByReducer { - type: T; - AS?: string; +interface GroupByReducerWithProperty extends GroupByReducer { + property: RediSearchProperty; } -type CountReducer = GroupByReducer; +type CountReducer = GroupByReducer; -interface CountDistinctReducer extends GroupByReducer { - property: PropertyName; -} +type CountDistinctReducer = GroupByReducerWithProperty; -interface CountDistinctishReducer extends GroupByReducer { - property: PropertyName; -} +type CountDistinctishReducer = GroupByReducerWithProperty; -interface SumReducer extends GroupByReducer { - property: PropertyName; -} +type SumReducer = GroupByReducerWithProperty; -interface MinReducer extends GroupByReducer { - property: PropertyName; -} +type MinReducer = GroupByReducerWithProperty; -interface MaxReducer extends GroupByReducer { - property: PropertyName; -} +type MaxReducer = GroupByReducerWithProperty; -interface AvgReducer extends GroupByReducer { - property: PropertyName; -} +type AvgReducer = GroupByReducerWithProperty; -interface StdDevReducer extends GroupByReducer { - property: PropertyName; -} +type StdDevReducer = GroupByReducerWithProperty; -interface QuantileReducer extends GroupByReducer { - property: PropertyName; - quantile: number; +interface QuantileReducer extends GroupByReducerWithProperty { + quantile: number; } -interface ToListReducer extends GroupByReducer { - property: PropertyName; -} +type ToListReducer = GroupByReducerWithProperty; -interface FirstValueReducer extends GroupByReducer { - property: PropertyName; - BY?: PropertyName | { - property: PropertyName; - direction?: 'ASC' | 'DESC'; - }; +interface FirstValueReducer extends GroupByReducerWithProperty { + BY?: RediSearchProperty | { + property: RediSearchProperty; + direction?: 'ASC' | 'DESC'; + }; } -interface RandomSampleReducer extends GroupByReducer { - property: PropertyName; - sampleSize: number; +interface RandomSampleReducer extends GroupByReducerWithProperty { + sampleSize: number; } type GroupByReducers = CountReducer | CountDistinctReducer | CountDistinctishReducer | SumReducer | MinReducer | MaxReducer | AvgReducer | StdDevReducer | QuantileReducer | ToListReducer | FirstValueReducer | RandomSampleReducer; -interface GroupByStep extends AggregateStep { - properties?: PropertyName | Array; - REDUCE: GroupByReducers | Array; +interface GroupByStep extends AggregateStep { + properties?: RediSearchProperty | Array; + REDUCE: GroupByReducers | Array; } -interface SortStep extends AggregateStep { - BY: SortByProperty | Array; - MAX?: number; -} +type SortByProperty = RedisArgument | { + BY: RediSearchProperty; + DIRECTION?: 'ASC' | 'DESC'; +}; -interface ApplyStep extends AggregateStep { - expression: string; - AS: string; +interface SortStep extends AggregateStep { + BY: SortByProperty | Array; + MAX?: number; } -interface LimitStep extends AggregateStep { - from: number; - size: number; +interface ApplyStep extends AggregateStep { + expression: RedisArgument; + AS: RedisArgument; } -interface FilterStep extends AggregateStep { - expression: string; +interface LimitStep extends AggregateStep { + from: number; + size: number; } -type LoadField = PropertyName | { - identifier: PropertyName; - AS?: string; +interface FilterStep extends AggregateStep { + expression: RedisArgument; } -export interface AggregateOptions { - VERBATIM?: boolean; - ADDSCORES?: boolean; - LOAD?: LoadField | Array; - STEPS?: Array; - PARAMS?: Params; - DIALECT?: number; - TIMEOUT?: number; +export interface FtAggregateOptions { + VERBATIM?: boolean; + ADDSCORES?: boolean; + LOAD?: LoadField | Array; + TIMEOUT?: number; + STEPS?: Array; + PARAMS?: FtSearchParams; + DIALECT?: number; } -export const FIRST_KEY_INDEX = 1; +export type AggregateRawReply = [ + total: UnwrapReply, + ...results: UnwrapReply>> +]; -export const IS_READ_ONLY = true; +export interface AggregateReply { + total: number; + results: Array>; +}; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: false, + transformArguments(index: RedisArgument, query: RedisArgument, options?: FtAggregateOptions) { + const args = ['FT.AGGREGATE', index, query]; + + return pushAggregateOptions(args, options); + }, + transformReply: { + 2: (rawReply: AggregateRawReply, preserve?: any, typeMapping?: TypeMapping): AggregateReply => { + const results: Array> = []; + for (let i = 1; i < rawReply.length; i++) { + results.push( + transformTuplesReply(rawReply[i] as ArrayReply, preserve, typeMapping) + ); + } + + return { + total: Number(rawReply[0]), + results + }; + }, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true +} as const satisfies Command; + +export function pushAggregateOptions(args: Array, options?: FtAggregateOptions) { + if (options?.VERBATIM) { + args.push('VERBATIM'); + } + + if (options?.ADDSCORES) { + args.push('ADDSCORES'); + } + + if (options?.LOAD) { + const length = args.push('LOAD', ''); + + if (Array.isArray(options.LOAD)) { + for (const load of options.LOAD) { + pushLoadField(args, load); + } + } else { + pushLoadField(args, options.LOAD); + } -export function transformArguments( - index: string, - query: string, - options?: AggregateOptions -): RedisCommandArguments { - return pushAggregatehOptions( - ['FT.AGGREGATE', index, query], - options - ); -} + args[length - 1] = (args.length - length).toString(); + } -export function pushAggregatehOptions( - args: RedisCommandArguments, - options?: AggregateOptions -): RedisCommandArguments { - if (options?.VERBATIM) { - args.push('VERBATIM'); - } + if (options?.TIMEOUT !== undefined) { + args.push('TIMEOUT', options.TIMEOUT.toString()); + } - if (options?.ADDSCORES) { - args.push('ADDSCORES'); - } + if (options?.STEPS) { + for (const step of options.STEPS) { + args.push(step.type); + switch (step.type) { + case FT_AGGREGATE_STEPS.GROUPBY: + if (!step.properties) { + args.push('0'); + } else { + pushVariadicArgument(args, step.properties); + } - if (options?.LOAD) { - args.push('LOAD'); - pushArgumentsWithLength(args, () => { - if (Array.isArray(options.LOAD)) { - for (const load of options.LOAD) { - pushLoadField(args, load); - } - } else { - pushLoadField(args, options.LOAD!); + if (Array.isArray(step.REDUCE)) { + for (const reducer of step.REDUCE) { + pushGroupByReducer(args, reducer); } - }); - } + } else { + pushGroupByReducer(args, step.REDUCE); + } - if (options?.STEPS) { - for (const step of options.STEPS) { - switch (step.type) { - case AggregateSteps.GROUPBY: - args.push('GROUPBY'); - if (!step.properties) { - args.push('0'); - } else { - pushVerdictArgument(args, step.properties); - } - - if (Array.isArray(step.REDUCE)) { - for (const reducer of step.REDUCE) { - pushGroupByReducer(args, reducer); - } - } else { - pushGroupByReducer(args, step.REDUCE); - } - - break; - - case AggregateSteps.SORTBY: - pushSortByArguments(args, 'SORTBY', step.BY); - - if (step.MAX) { - args.push('MAX', step.MAX.toString()); - } - - break; - - case AggregateSteps.APPLY: - args.push('APPLY', step.expression, 'AS', step.AS); - break; - - case AggregateSteps.LIMIT: - args.push('LIMIT', step.from.toString(), step.size.toString()); - break; - - case AggregateSteps.FILTER: - args.push('FILTER', step.expression); - break; + break; + + case FT_AGGREGATE_STEPS.SORTBY: + const length = args.push(''); + + if (Array.isArray(step.BY)) { + for (const by of step.BY) { + pushSortByProperty(args, by); } - } - } + } else { + pushSortByProperty(args, step.BY); + } - pushParamsArgs(args, options?.PARAMS); + if (step.MAX) { + args.push('MAX', step.MAX.toString()); + } - if (options?.DIALECT) { - args.push('DIALECT', options.DIALECT.toString()); - } + args[length - 1] = (args.length - length).toString(); + + break; - if (options?.TIMEOUT !== undefined) { - args.push('TIMEOUT', options.TIMEOUT.toString()); + case FT_AGGREGATE_STEPS.APPLY: + args.push(step.expression, 'AS', step.AS); + break; + + case FT_AGGREGATE_STEPS.LIMIT: + args.push(step.from.toString(), step.size.toString()); + break; + + case FT_AGGREGATE_STEPS.FILTER: + args.push(step.expression); + break; + } } + } + + pushParamsArgument(args, options?.PARAMS); - return args; + if (options?.DIALECT !== undefined) { + args.push('DIALECT', options.DIALECT.toString()); + } + + return args; } -function pushLoadField(args: RedisCommandArguments, toLoad: LoadField): void { - if (typeof toLoad === 'string') { - args.push(toLoad); - } else { - args.push(toLoad.identifier); +function pushLoadField(args: Array, toLoad: LoadField) { + if (typeof toLoad === 'string' || toLoad instanceof Buffer) { + args.push(toLoad); + } else { + args.push(toLoad.identifier); - if (toLoad.AS) { - args.push('AS', toLoad.AS); - } + if (toLoad.AS) { + args.push('AS', toLoad.AS); } + } } -function pushGroupByReducer(args: RedisCommandArguments, reducer: GroupByReducers): void { - args.push('REDUCE', reducer.type); - - switch (reducer.type) { - case AggregateGroupByReducers.COUNT: - args.push('0'); - break; - - case AggregateGroupByReducers.COUNT_DISTINCT: - case AggregateGroupByReducers.COUNT_DISTINCTISH: - case AggregateGroupByReducers.SUM: - case AggregateGroupByReducers.MIN: - case AggregateGroupByReducers.MAX: - case AggregateGroupByReducers.AVG: - case AggregateGroupByReducers.STDDEV: - case AggregateGroupByReducers.TOLIST: - args.push('1', reducer.property); - break; - - case AggregateGroupByReducers.QUANTILE: - args.push('2', reducer.property, reducer.quantile.toString()); - break; - - case AggregateGroupByReducers.FIRST_VALUE: { - pushArgumentsWithLength(args, () => { - args.push(reducer.property); - - if (reducer.BY) { - args.push('BY'); - if (typeof reducer.BY === 'string') { - args.push(reducer.BY); - } else { - args.push(reducer.BY.property); - - if (reducer.BY.direction) { - args.push(reducer.BY.direction); - } - } - } - }); - break; +function pushGroupByReducer(args: Array, reducer: GroupByReducers) { + args.push('REDUCE', reducer.type); + + switch (reducer.type) { + case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT: + args.push('0'); + break; + + case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT_DISTINCT: + case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT_DISTINCTISH: + case FT_AGGREGATE_GROUP_BY_REDUCERS.SUM: + case FT_AGGREGATE_GROUP_BY_REDUCERS.MIN: + case FT_AGGREGATE_GROUP_BY_REDUCERS.MAX: + case FT_AGGREGATE_GROUP_BY_REDUCERS.AVG: + case FT_AGGREGATE_GROUP_BY_REDUCERS.STDDEV: + case FT_AGGREGATE_GROUP_BY_REDUCERS.TOLIST: + args.push('1', reducer.property); + break; + + case FT_AGGREGATE_GROUP_BY_REDUCERS.QUANTILE: + args.push('2', reducer.property, reducer.quantile.toString()); + break; + + case FT_AGGREGATE_GROUP_BY_REDUCERS.FIRST_VALUE: { + const length = args.push('', reducer.property) - 1; + if (reducer.BY) { + args.push('BY'); + if (typeof reducer.BY === 'string' || reducer.BY instanceof Buffer) { + args.push(reducer.BY); + } else { + args.push(reducer.BY.property); + if (reducer.BY.direction) { + args.push(reducer.BY.direction); + } } + } - case AggregateGroupByReducers.RANDOM_SAMPLE: - args.push('2', reducer.property, reducer.sampleSize.toString()); - break; - } - - if (reducer.AS) { - args.push('AS', reducer.AS); + args[length - 1] = (args.length - length).toString(); + break; } -} -export type AggregateRawReply = [ - total: number, - ...results: Array> -]; + case FT_AGGREGATE_GROUP_BY_REDUCERS.RANDOM_SAMPLE: + args.push('2', reducer.property, reducer.sampleSize.toString()); + break; + } -export interface AggregateReply { - total: number; - results: Array>; + if (reducer.AS) { + args.push('AS', reducer.AS); + } } -export function transformReply(rawReply: AggregateRawReply): AggregateReply { - const results: Array> = []; - for (let i = 1; i < rawReply.length; i++) { - results.push( - transformTuplesReply(rawReply[i] as Array) - ); +function pushSortByProperty(args: Array, sortBy: SortByProperty) { + if (typeof sortBy === 'string' || sortBy instanceof Buffer) { + args.push(sortBy); + } else { + args.push(sortBy.BY); + if (sortBy.DIRECTION) { + args.push(sortBy.DIRECTION); } - - return { - total: rawReply[0], - results - }; + } } diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts index 65396f3f790..9db3d945f97 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts @@ -1,37 +1,47 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './AGGREGATE_WITHCURSOR'; -import { SchemaFieldTypes } from '.'; +import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR'; describe('AGGREGATE WITHCURSOR', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('index', '*'), - ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR'] - ); - }); + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + AGGREGATE_WITHCURSOR.transformArguments('index', '*'), + ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR'] + ); + }); + + it('with COUNT', () => { + assert.deepEqual( + AGGREGATE_WITHCURSOR.transformArguments('index', '*', { + COUNT: 1 + }), + ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'COUNT', '1'] + ); + }); - it('with COUNT', () => { - assert.deepEqual( - transformArguments('index', '*', { COUNT: 1 }), - ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'COUNT', '1'] - ); - }); + it('with MAXIDLE', () => { + assert.deepEqual( + AGGREGATE_WITHCURSOR.transformArguments('index', '*', { + MAXIDLE: 1 + }), + ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'MAXIDLE', '1'] + ); }); + }); - testUtils.testWithClient('client.ft.aggregateWithCursor', async client => { - await client.ft.create('index', { - field: SchemaFieldTypes.NUMERIC - }); + testUtils.testWithClient('client.ft.aggregateWithCursor', async client => { + await client.ft.create('index', { + field: 'NUMERIC' + }); - assert.deepEqual( - await client.ft.aggregateWithCursor('index', '*'), - { - total: 0, - results: [], - cursor: 0 - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + await client.ft.aggregateWithCursor('index', '*'), + { + total: 0, + results: [], + cursor: 0 + } + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts index 63f6ee8f187..cffb86b8b44 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts @@ -1,44 +1,47 @@ -import { - AggregateOptions, - AggregateRawReply, - AggregateReply, - transformArguments as transformAggregateArguments, - transformReply as transformAggregateReply -} from './AGGREGATE'; - -export { FIRST_KEY_INDEX, IS_READ_ONLY } from './AGGREGATE'; - -interface AggregateWithCursorOptions extends AggregateOptions { - COUNT?: number; -} - -export function transformArguments( - index: string, - query: string, - options?: AggregateWithCursorOptions -) { - const args = transformAggregateArguments(index, query, options); - - args.push('WITHCURSOR'); - if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); - } +import { RedisArgument, Command, ReplyUnion, NumberReply } from '@redis/client/dist/lib/RESP/types'; +import AGGREGATE, { AggregateRawReply, AggregateReply, FtAggregateOptions } from './AGGREGATE'; - return args; +export interface FtAggregateWithCursorOptions extends FtAggregateOptions { + COUNT?: number; + MAXIDLE?: number; } + type AggregateWithCursorRawReply = [ - result: AggregateRawReply, - cursor: number + result: AggregateRawReply, + cursor: NumberReply ]; -interface AggregateWithCursorReply extends AggregateReply { - cursor: number; +export interface AggregateWithCursorReply extends AggregateReply { + cursor: NumberReply; } -export function transformReply(reply: AggregateWithCursorRawReply): AggregateWithCursorReply { - return { - ...transformAggregateReply(reply[0]), +export default { + FIRST_KEY_INDEX: AGGREGATE.FIRST_KEY_INDEX, + IS_READ_ONLY: AGGREGATE.IS_READ_ONLY, + transformArguments(index: RedisArgument, query: RedisArgument, options?: FtAggregateWithCursorOptions) { + const args = AGGREGATE.transformArguments(index, query, options); + args.push('WITHCURSOR'); + + if (options?.COUNT !== undefined) { + args.push('COUNT', options.COUNT.toString()); + } + + if(options?.MAXIDLE !== undefined) { + args.push('MAXIDLE', options.MAXIDLE.toString()); + } + + return args; + }, + transformReply: { + 2: (reply: AggregateWithCursorRawReply): AggregateWithCursorReply => { + return { + ...AGGREGATE.transformReply[2](reply[0]), cursor: reply[1] - }; -} + }; + }, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true +} as const satisfies Command; + diff --git a/packages/search/lib/commands/ALIASADD.spec.ts b/packages/search/lib/commands/ALIASADD.spec.ts index 7bb2452838b..3a5d02175f9 100644 --- a/packages/search/lib/commands/ALIASADD.spec.ts +++ b/packages/search/lib/commands/ALIASADD.spec.ts @@ -1,11 +1,24 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './ALIASADD'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ALIASADD from './ALIASADD'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; -describe('ALIASADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('alias', 'index'), - ['FT.ALIASADD', 'alias', 'index'] - ); - }); +describe('FT.ALIASADD', () => { + it('transformArguments', () => { + assert.deepEqual( + ALIASADD.transformArguments('alias', 'index'), + ['FT.ALIASADD', 'alias', 'index'] + ); + }); + + testUtils.testWithClient('client.ft.aliasAdd', async client => { + const [, reply] = await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }), + client.ft.aliasAdd('alias', 'index') + ]); + + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/ALIASADD.ts b/packages/search/lib/commands/ALIASADD.ts index 552c1add695..648e1fef97e 100644 --- a/packages/search/lib/commands/ALIASADD.ts +++ b/packages/search/lib/commands/ALIASADD.ts @@ -1,5 +1,10 @@ -export function transformArguments(name: string, index: string): Array { - return ['FT.ALIASADD', name, index]; -} +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(alias: RedisArgument, index: RedisArgument) { + return ['FT.ALIASADD', alias, index]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/search/lib/commands/ALIASDEL.spec.ts b/packages/search/lib/commands/ALIASDEL.spec.ts index 5255ba835db..3842d01b145 100644 --- a/packages/search/lib/commands/ALIASDEL.spec.ts +++ b/packages/search/lib/commands/ALIASDEL.spec.ts @@ -1,11 +1,25 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './ALIASDEL'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ALIASDEL from './ALIASDEL'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; -describe('ALIASDEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('alias', 'index'), - ['FT.ALIASDEL', 'alias', 'index'] - ); - }); +describe('FT.ALIASDEL', () => { + it('transformArguments', () => { + assert.deepEqual( + ALIASDEL.transformArguments('alias'), + ['FT.ALIASDEL', 'alias'] + ); + }); + + testUtils.testWithClient('client.ft.aliasAdd', async client => { + const [, , reply] = await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }), + client.ft.aliasAdd('alias', 'index'), + client.ft.aliasDel('alias') + ]); + + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/ALIASDEL.ts b/packages/search/lib/commands/ALIASDEL.ts index 434b4df3dea..40cc45a19de 100644 --- a/packages/search/lib/commands/ALIASDEL.ts +++ b/packages/search/lib/commands/ALIASDEL.ts @@ -1,5 +1,10 @@ -export function transformArguments(name: string, index: string): Array { - return ['FT.ALIASDEL', name, index]; -} +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(alias: RedisArgument) { + return ['FT.ALIASDEL', alias]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/search/lib/commands/ALIASUPDATE.spec.ts b/packages/search/lib/commands/ALIASUPDATE.spec.ts index 79421b1a20d..a0e7431af6b 100644 --- a/packages/search/lib/commands/ALIASUPDATE.spec.ts +++ b/packages/search/lib/commands/ALIASUPDATE.spec.ts @@ -1,11 +1,24 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './ALIASUPDATE'; +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import ALIASUPDATE from './ALIASUPDATE'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; -describe('ALIASUPDATE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('alias', 'index'), - ['FT.ALIASUPDATE', 'alias', 'index'] - ); - }); +describe('FT.ALIASUPDATE', () => { + it('transformArguments', () => { + assert.deepEqual( + ALIASUPDATE.transformArguments('alias', 'index'), + ['FT.ALIASUPDATE', 'alias', 'index'] + ); + }); + + testUtils.testWithClient('client.ft.aliasUpdate', async client => { + const [, reply] = await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }), + client.ft.aliasUpdate('alias', 'index') + ]); + + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/ALIASUPDATE.ts b/packages/search/lib/commands/ALIASUPDATE.ts index ac64ef57c3f..e2b72cfe649 100644 --- a/packages/search/lib/commands/ALIASUPDATE.ts +++ b/packages/search/lib/commands/ALIASUPDATE.ts @@ -1,5 +1,10 @@ -export function transformArguments(name: string, index: string): Array { - return ['FT.ALIASUPDATE', name, index]; -} +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(alias: RedisArgument, index: RedisArgument) { + return ['FT.ALIASUPDATE', alias, index]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/search/lib/commands/ALTER.spec.ts b/packages/search/lib/commands/ALTER.spec.ts index e9724757ad7..6cac0be40c8 100644 --- a/packages/search/lib/commands/ALTER.spec.ts +++ b/packages/search/lib/commands/ALTER.spec.ts @@ -1,37 +1,35 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ALTER'; -import { SchemaFieldTypes } from '.'; +import ALTER from './ALTER'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; -describe('ALTER', () => { - describe('transformArguments', () => { - it('with NOINDEX', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - NOINDEX: true, - SORTABLE: 'UNF', - AS: 'text' - } - }), - ['FT.ALTER', 'index', 'SCHEMA', 'ADD', 'field', 'AS', 'text', 'TEXT', 'SORTABLE', 'UNF', 'NOINDEX'] - ); - }); +describe('FT.ALTER', () => { + describe('transformArguments', () => { + it('with NOINDEX', () => { + assert.deepEqual( + ALTER.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + NOINDEX: true, + SORTABLE: 'UNF', + AS: 'text' + } + }), + ['FT.ALTER', 'index', 'SCHEMA', 'ADD', 'field', 'AS', 'text', 'TEXT', 'SORTABLE', 'UNF', 'NOINDEX'] + ); }); + }); - testUtils.testWithClient('client.ft.create', async client => { - await Promise.all([ - client.ft.create('index', { - title: SchemaFieldTypes.TEXT - }), - ]); + testUtils.testWithClient('client.ft.create', async client => { + const [, reply] = await Promise.all([ + client.ft.create('index', { + title: SCHEMA_FIELD_TYPE.TEXT + }), + client.ft.alter('index', { + body: SCHEMA_FIELD_TYPE.TEXT + }) + ]); - assert.equal( - await client.ft.alter('index', { - body: SchemaFieldTypes.TEXT - }), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/ALTER.ts b/packages/search/lib/commands/ALTER.ts index bb4c5202c65..d5587b2397c 100644 --- a/packages/search/lib/commands/ALTER.ts +++ b/packages/search/lib/commands/ALTER.ts @@ -1,10 +1,13 @@ -import { RediSearchSchema, pushSchema } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RediSearchSchema, pushSchema } from './CREATE'; -export function transformArguments(index: string, schema: RediSearchSchema): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument, schema: RediSearchSchema) { const args = ['FT.ALTER', index, 'SCHEMA', 'ADD']; pushSchema(args, schema); - return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/search/lib/commands/CONFIG_GET.spec.ts b/packages/search/lib/commands/CONFIG_GET.spec.ts index 8614f443426..7ef2a3536b9 100644 --- a/packages/search/lib/commands/CONFIG_GET.spec.ts +++ b/packages/search/lib/commands/CONFIG_GET.spec.ts @@ -1,25 +1,25 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CONFIG_GET'; +import CONFIG_GET from './CONFIG_GET'; -describe('CONFIG GET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('TIMEOUT'), - ['FT.CONFIG', 'GET', 'TIMEOUT'] - ); - }); +describe('FT.CONFIG GET', () => { + it('transformArguments', () => { + assert.deepEqual( + CONFIG_GET.transformArguments('TIMEOUT'), + ['FT.CONFIG', 'GET', 'TIMEOUT'] + ); + }); - testUtils.testWithClient('client.ft.configGet', async client => { - assert.deepEqual( - await client.ft.configGet('TIMEOUT'), - Object.create(null, { - TIMEOUT: { - value: '500', - configurable: true, - enumerable: true - } - }) - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ft.configGet', async client => { + assert.deepEqual( + await client.ft.configGet('TIMEOUT'), + Object.create(null, { + TIMEOUT: { + value: '500', + configurable: true, + enumerable: true + } + }) + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/CONFIG_GET.ts b/packages/search/lib/commands/CONFIG_GET.ts index fbf1f1164b9..f96461e8694 100644 --- a/packages/search/lib/commands/CONFIG_GET.ts +++ b/packages/search/lib/commands/CONFIG_GET.ts @@ -1,16 +1,18 @@ -export function transformArguments(option: string) { - return ['FT.CONFIG', 'GET', option]; -} - -interface ConfigGetReply { - [option: string]: string | null; -} +import { ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformReply(rawReply: Array<[string, string | null]>): ConfigGetReply { - const transformedReply: ConfigGetReply = Object.create(null); - for (const [key, value] of rawReply) { - transformedReply[key] = value; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(option: string) { + return ['FT.CONFIG', 'GET', option]; + }, + transformReply(reply: UnwrapReply>>) { + const transformedReply: Record = Object.create(null); + for (const item of reply) { + const [key, value] = item as unknown as UnwrapReply; + transformedReply[key.toString()] = value; } return transformedReply; -} + } +} as const satisfies Command; diff --git a/packages/search/lib/commands/CONFIG_SET.spec.ts b/packages/search/lib/commands/CONFIG_SET.spec.ts index 59cb63a3d8e..3b20f2eac5d 100644 --- a/packages/search/lib/commands/CONFIG_SET.spec.ts +++ b/packages/search/lib/commands/CONFIG_SET.spec.ts @@ -1,12 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CONFIG_SET'; +import CONFIG_SET from './CONFIG_SET'; -describe('CONFIG SET', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('TIMEOUT', '500'), - ['FT.CONFIG', 'SET', 'TIMEOUT', '500'] - ); - }); +describe('FT.CONFIG SET', () => { + it('transformArguments', () => { + assert.deepEqual( + CONFIG_SET.transformArguments('TIMEOUT', '500'), + ['FT.CONFIG', 'SET', 'TIMEOUT', '500'] + ); + }); + + testUtils.testWithClient('client.ft.configSet', async client => { + assert.deepEqual( + await client.ft.configSet('TIMEOUT', '500'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/CONFIG_SET.ts b/packages/search/lib/commands/CONFIG_SET.ts index 93b76d79edf..ac001bf68a6 100644 --- a/packages/search/lib/commands/CONFIG_SET.ts +++ b/packages/search/lib/commands/CONFIG_SET.ts @@ -1,5 +1,14 @@ -export function transformArguments(option: string, value: string): Array { - return ['FT.CONFIG', 'SET', option, value]; -} +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): 'OK'; +// using `string & {}` to avoid TS widening the type to `string` +// TODO +type FtConfigProperties = 'a' | 'b' | (string & {}) | Buffer; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(property: FtConfigProperties, value: RedisArgument) { + return ['FT.CONFIG', 'SET', property, value]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/search/lib/commands/CREATE.spec.ts b/packages/search/lib/commands/CREATE.spec.ts index 50c5c011c89..bc48691bd58 100644 --- a/packages/search/lib/commands/CREATE.spec.ts +++ b/packages/search/lib/commands/CREATE.spec.ts @@ -1,490 +1,475 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CREATE'; -import { SchemaFieldTypes, SchemaTextFieldPhonetics, RedisSearchLanguages, VectorAlgorithms, SCHEMA_GEO_SHAPE_COORD_SYSTEM } from '.'; +import CREATE, { SCHEMA_FIELD_TYPE, SCHEMA_TEXT_FIELD_PHONETIC, SCHEMA_VECTOR_FIELD_ALGORITHM, REDISEARCH_LANGUAGE } from './CREATE'; + +describe('FT.CREATE', () => { + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}), + ['FT.CREATE', 'index', 'SCHEMA'] + ); + }); -describe('CREATE', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('index', {}), - ['FT.CREATE', 'index', 'SCHEMA'] - ); + describe('with fields', () => { + describe('TEXT', () => { + it('without options', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT'] + ); }); - describe('with fields', () => { - describe('TEXT', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('index', { - field: SchemaFieldTypes.TEXT - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT'] - ); - }); - - it('with NOSTEM', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - NOSTEM: true - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOSTEM'] - ); - }); - - it('with WEIGHT', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - WEIGHT: 1 - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WEIGHT', '1'] - ); - }); - - it('with PHONETIC', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - PHONETIC: SchemaTextFieldPhonetics.DM_EN - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'PHONETIC', SchemaTextFieldPhonetics.DM_EN] - ); - }); - - it('with WITHSUFFIXTRIE', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - WITHSUFFIXTRIE: true - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WITHSUFFIXTRIE'] - ); - }); - - it('with INDEXEMPTY', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - INDEXEMPTY: true - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXEMPTY'] - ); - }); - }); - - it('NUMERIC', () => { - assert.deepEqual( - transformArguments('index', { - field: SchemaFieldTypes.NUMERIC - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'NUMERIC'] - ); - }); - - it('GEO', () => { - assert.deepEqual( - transformArguments('index', { - field: SchemaFieldTypes.GEO - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEO'] - ); - }); - - describe('TAG', () => { - describe('without options', () => { - it('SchemaFieldTypes.TAG', () => { - assert.deepEqual( - transformArguments('index', { - field: SchemaFieldTypes.TAG - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG'] - ); - }); - - it('{ type: SchemaFieldTypes.TAG }', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TAG - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG'] - ); - }); - }); - - it('with SEPARATOR', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TAG, - SEPARATOR: 'separator' - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'SEPARATOR', 'separator'] - ); - }); - - it('with CASESENSITIVE', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TAG, - CASESENSITIVE: true - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'CASESENSITIVE'] - ); - }); - - it('with WITHSUFFIXTRIE', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TAG, - WITHSUFFIXTRIE: true - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'WITHSUFFIXTRIE'] - ); - }); - - it('with INDEXEMPTY', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TAG, - INDEXEMPTY: true - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'INDEXEMPTY'] - ); - }); - }); - - describe('VECTOR', () => { - it('Flat algorithm', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.VECTOR, - ALGORITHM: VectorAlgorithms.FLAT, - TYPE: 'FLOAT32', - DIM: 2, - DISTANCE_METRIC: 'L2', - INITIAL_CAP: 1000000, - BLOCK_SIZE: 1000 - } - }), - [ - 'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'FLAT', '10', 'TYPE', - 'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000', - 'BLOCK_SIZE', '1000' - ] - ); - }); - - it('HNSW algorithm', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.VECTOR, - ALGORITHM: VectorAlgorithms.HNSW, - TYPE: 'FLOAT32', - DIM: 2, - DISTANCE_METRIC: 'L2', - INITIAL_CAP: 1000000, - M: 40, - EF_CONSTRUCTION: 250, - EF_RUNTIME: 20 - } - }), - [ - 'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'HNSW', '14', 'TYPE', - 'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000', - 'M', '40', 'EF_CONSTRUCTION', '250', 'EF_RUNTIME', '20' - ] - ); - }); - }); - - describe('GEOSHAPE', () => { - describe('without options', () => { - it('SCHEMA_FIELD_TYPE.GEOSHAPE', () => { - assert.deepEqual( - transformArguments('index', { - field: SchemaFieldTypes.GEOSHAPE - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE'] - ); - }); - - it('{ type: SCHEMA_FIELD_TYPE.GEOSHAPE }', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.GEOSHAPE - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE'] - ); - }); - }); - - it('with COORD_SYSTEM', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.GEOSHAPE, - COORD_SYSTEM: SCHEMA_GEO_SHAPE_COORD_SYSTEM.SPHERICAL - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE', 'COORD_SYSTEM', 'SPHERICAL'] - ); - }); - }); - - describe('with generic options', () => { - it('with AS', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - AS: 'as' - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'AS', 'as', 'TEXT'] - ); - }); - - describe('with SORTABLE', () => { - it('true', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - SORTABLE: true - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE'] - ); - }); - - it('UNF', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - SORTABLE: 'UNF' - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE', 'UNF'] - ); - }); - }); - - it('with NOINDEX', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - NOINDEX: true - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOINDEX'] - ); - }); - - it('with INDEXMISSING', () => { - assert.deepEqual( - transformArguments('index', { - field: { - type: SchemaFieldTypes.TEXT, - INDEXMISSING: true - } - }), - ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXMISSING'] - ); - }); - }); + it('with NOSTEM', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + NOSTEM: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOSTEM'] + ); }); - it('with ON', () => { - assert.deepEqual( - transformArguments('index', {}, { - ON: 'HASH' - }), - ['FT.CREATE', 'index', 'ON', 'HASH', 'SCHEMA'] - ); + it('with WEIGHT', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + WEIGHT: 1 + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WEIGHT', '1'] + ); }); - describe('with PREFIX', () => { - it('string', () => { - assert.deepEqual( - transformArguments('index', {}, { - PREFIX: 'prefix' - }), - ['FT.CREATE', 'index', 'PREFIX', '1', 'prefix', 'SCHEMA'] - ); - }); - - it('Array', () => { - assert.deepEqual( - transformArguments('index', {}, { - PREFIX: ['1', '2'] - }), - ['FT.CREATE', 'index', 'PREFIX', '2', '1', '2', 'SCHEMA'] - ); - }); + it('with PHONETIC', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + PHONETIC: SCHEMA_TEXT_FIELD_PHONETIC.DM_EN + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'PHONETIC', SCHEMA_TEXT_FIELD_PHONETIC.DM_EN] + ); }); - it('with FILTER', () => { - assert.deepEqual( - transformArguments('index', {}, { - FILTER: '@field != ""' - }), - ['FT.CREATE', 'index', 'FILTER', '@field != ""', 'SCHEMA'] - ); + it('with WITHSUFFIXTRIE', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + WITHSUFFIXTRIE: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WITHSUFFIXTRIE'] + ); }); + }); + + it('NUMERIC', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: SCHEMA_FIELD_TYPE.NUMERIC + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'NUMERIC'] + ); + }); + + it('GEO', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: SCHEMA_FIELD_TYPE.GEO + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEO'] + ); + }); - it('with LANGUAGE', () => { + describe('TAG', () => { + describe('without options', () => { + it('SCHEMA_FIELD_TYPE.TAG', () => { assert.deepEqual( - transformArguments('index', {}, { - LANGUAGE: RedisSearchLanguages.ARABIC - }), - ['FT.CREATE', 'index', 'LANGUAGE', RedisSearchLanguages.ARABIC, 'SCHEMA'] + CREATE.transformArguments('index', { + field: SCHEMA_FIELD_TYPE.TAG + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG'] ); - }); + }); - it('with LANGUAGE_FIELD', () => { + it('{ type: SCHEMA_FIELD_TYPE.TAG }', () => { assert.deepEqual( - transformArguments('index', {}, { - LANGUAGE_FIELD: '@field' - }), - ['FT.CREATE', 'index', 'LANGUAGE_FIELD', '@field', 'SCHEMA'] + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TAG + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG'] ); + }); }); - it('with SCORE', () => { - assert.deepEqual( - transformArguments('index', {}, { - SCORE: 1 - }), - ['FT.CREATE', 'index', 'SCORE', '1', 'SCHEMA'] - ); + it('with SEPARATOR', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TAG, + SEPARATOR: 'separator' + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'SEPARATOR', 'separator'] + ); }); - it('with SCORE_FIELD', () => { - assert.deepEqual( - transformArguments('index', {}, { - SCORE_FIELD: '@field' - }), - ['FT.CREATE', 'index', 'SCORE_FIELD', '@field', 'SCHEMA'] - ); + it('with CASESENSITIVE', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TAG, + CASESENSITIVE: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'CASESENSITIVE'] + ); }); - it('with MAXTEXTFIELDS', () => { - assert.deepEqual( - transformArguments('index', {}, { - MAXTEXTFIELDS: true - }), - ['FT.CREATE', 'index', 'MAXTEXTFIELDS', 'SCHEMA'] - ); + it('with WITHSUFFIXTRIE', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TAG, + WITHSUFFIXTRIE: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'WITHSUFFIXTRIE'] + ); }); - it('with TEMPORARY', () => { - assert.deepEqual( - transformArguments('index', {}, { - TEMPORARY: 1 - }), - ['FT.CREATE', 'index', 'TEMPORARY', '1', 'SCHEMA'] - ); + it('with INDEXEMPTY', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TAG, + INDEXEMPTY: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'INDEXEMPTY'] + ); }); - - it('with NOOFFSETS', () => { - assert.deepEqual( - transformArguments('index', {}, { - NOOFFSETS: true - }), - ['FT.CREATE', 'index', 'NOOFFSETS', 'SCHEMA'] - ); + }); + + describe('VECTOR', () => { + it('Flat algorithm', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.VECTOR, + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT, + TYPE: 'FLOAT32', + DIM: 2, + DISTANCE_METRIC: 'L2', + INITIAL_CAP: 1000000, + BLOCK_SIZE: 1000 + } + }), + [ + 'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'FLAT', '10', 'TYPE', + 'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000', + 'BLOCK_SIZE', '1000' + ] + ); }); - it('with NOHL', () => { - assert.deepEqual( - transformArguments('index', {}, { - NOHL: true - }), - ['FT.CREATE', 'index', 'NOHL', 'SCHEMA'] - ); + it('HNSW algorithm', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.VECTOR, + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW, + TYPE: 'FLOAT32', + DIM: 2, + DISTANCE_METRIC: 'L2', + INITIAL_CAP: 1000000, + M: 40, + EF_CONSTRUCTION: 250, + EF_RUNTIME: 20 + } + }), + [ + 'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'HNSW', '14', 'TYPE', + 'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000', + 'M', '40', 'EF_CONSTRUCTION', '250', 'EF_RUNTIME', '20' + ] + ); }); + }); - it('with NOFIELDS', () => { + describe('GEOSHAPE', () => { + describe('without options', () => { + it('SCHEMA_FIELD_TYPE.GEOSHAPE', () => { assert.deepEqual( - transformArguments('index', {}, { - NOFIELDS: true - }), - ['FT.CREATE', 'index', 'NOFIELDS', 'SCHEMA'] + CREATE.transformArguments('index', { + field: SCHEMA_FIELD_TYPE.GEOSHAPE + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE'] ); - }); + }); - it('with NOFREQS', () => { + it('{ type: SCHEMA_FIELD_TYPE.GEOSHAPE }', () => { assert.deepEqual( - transformArguments('index', {}, { - NOFREQS: true - }), - ['FT.CREATE', 'index', 'NOFREQS', 'SCHEMA'] + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.GEOSHAPE + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE'] ); + }); }); - it('with SKIPINITIALSCAN', () => { - assert.deepEqual( - transformArguments('index', {}, { - SKIPINITIALSCAN: true - }), - ['FT.CREATE', 'index', 'SKIPINITIALSCAN', 'SCHEMA'] - ); + it('with COORD_SYSTEM', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.GEOSHAPE, + COORD_SYSTEM: 'SPHERICAL' + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE', 'COORD_SYSTEM', 'SPHERICAL'] + ); + }); + }); + + it('with AS', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + AS: 'as' + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'AS', 'as', 'TEXT'] + ); + }); + + describe('with SORTABLE', () => { + it('true', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + SORTABLE: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE'] + ); }); - describe('with STOPWORDS', () => { - it('string', () => { - assert.deepEqual( - transformArguments('index', {}, { - STOPWORDS: 'stopword' - }), - ['FT.CREATE', 'index', 'STOPWORDS', '1', 'stopword', 'SCHEMA'] - ); - }); - - it('Array', () => { - assert.deepEqual( - transformArguments('index', {}, { - STOPWORDS: ['1', '2'] - }), - ['FT.CREATE', 'index', 'STOPWORDS', '2', '1', '2', 'SCHEMA'] - ); - }); + it('UNF', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + SORTABLE: 'UNF' + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE', 'UNF'] + ); }); + }); + + it('with NOINDEX', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + NOINDEX: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOINDEX'] + ); + }); + + it('with INDEXMISSING', () => { + assert.deepEqual( + CREATE.transformArguments('index', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT, + INDEXMISSING: true + } + }), + ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXMISSING'] + ); + }); }); - testUtils.testWithClient('client.ft.create', async client => { - assert.equal( - await client.ft.create('index', { - field: SchemaFieldTypes.TEXT - }), - 'OK' + it('with ON', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + ON: 'HASH' + }), + ['FT.CREATE', 'index', 'ON', 'HASH', 'SCHEMA'] + ); + }); + + describe('with PREFIX', () => { + it('string', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + PREFIX: 'prefix' + }), + ['FT.CREATE', 'index', 'PREFIX', '1', 'prefix', 'SCHEMA'] + ); + }); + + it('Array', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + PREFIX: ['1', '2'] + }), + ['FT.CREATE', 'index', 'PREFIX', '2', '1', '2', 'SCHEMA'] + ); + }); + }); + + it('with FILTER', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + FILTER: '@field != ""' + }), + ['FT.CREATE', 'index', 'FILTER', '@field != ""', 'SCHEMA'] + ); + }); + + it('with LANGUAGE', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + LANGUAGE: REDISEARCH_LANGUAGE.ARABIC + }), + ['FT.CREATE', 'index', 'LANGUAGE', REDISEARCH_LANGUAGE.ARABIC, 'SCHEMA'] + ); + }); + + it('with LANGUAGE_FIELD', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + LANGUAGE_FIELD: '@field' + }), + ['FT.CREATE', 'index', 'LANGUAGE_FIELD', '@field', 'SCHEMA'] + ); + }); + + it('with SCORE', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + SCORE: 1 + }), + ['FT.CREATE', 'index', 'SCORE', '1', 'SCHEMA'] + ); + }); + + it('with SCORE_FIELD', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + SCORE_FIELD: '@field' + }), + ['FT.CREATE', 'index', 'SCORE_FIELD', '@field', 'SCHEMA'] + ); + }); + + it('with MAXTEXTFIELDS', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + MAXTEXTFIELDS: true + }), + ['FT.CREATE', 'index', 'MAXTEXTFIELDS', 'SCHEMA'] + ); + }); + + it('with TEMPORARY', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + TEMPORARY: 1 + }), + ['FT.CREATE', 'index', 'TEMPORARY', '1', 'SCHEMA'] + ); + }); + + it('with NOOFFSETS', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + NOOFFSETS: true + }), + ['FT.CREATE', 'index', 'NOOFFSETS', 'SCHEMA'] + ); + }); + + it('with NOHL', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + NOHL: true + }), + ['FT.CREATE', 'index', 'NOHL', 'SCHEMA'] + ); + }); + + it('with NOFIELDS', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + NOFIELDS: true + }), + ['FT.CREATE', 'index', 'NOFIELDS', 'SCHEMA'] + ); + }); + + it('with NOFREQS', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + NOFREQS: true + }), + ['FT.CREATE', 'index', 'NOFREQS', 'SCHEMA'] + ); + }); + + it('with SKIPINITIALSCAN', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + SKIPINITIALSCAN: true + }), + ['FT.CREATE', 'index', 'SKIPINITIALSCAN', 'SCHEMA'] + ); + }); + + describe('with STOPWORDS', () => { + it('string', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + STOPWORDS: 'stopword' + }), + ['FT.CREATE', 'index', 'STOPWORDS', '1', 'stopword', 'SCHEMA'] ); - }, GLOBAL.SERVERS.OPEN); + }); + + it('Array', () => { + assert.deepEqual( + CREATE.transformArguments('index', {}, { + STOPWORDS: ['1', '2'] + }), + ['FT.CREATE', 'index', 'STOPWORDS', '2', '1', '2', 'SCHEMA'] + ); + }); + }); + }); + + testUtils.testWithClient('client.ft.create', async client => { + assert.equal( + await client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/CREATE.ts b/packages/search/lib/commands/CREATE.ts index 21662c28d7d..2951e56f090 100644 --- a/packages/search/lib/commands/CREATE.ts +++ b/packages/search/lib/commands/CREATE.ts @@ -1,52 +1,323 @@ -import { pushOptionalVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { RedisSearchLanguages, PropertyName, RediSearchSchema, pushSchema } from '.'; - -interface CreateOptions { - ON?: 'HASH' | 'JSON'; - PREFIX?: string | Array; - FILTER?: string; - LANGUAGE?: RedisSearchLanguages; - LANGUAGE_FIELD?: PropertyName; - SCORE?: number; - SCORE_FIELD?: PropertyName; - // PAYLOAD_FIELD?: string; - MAXTEXTFIELDS?: true; - TEMPORARY?: number; - NOOFFSETS?: true; - NOHL?: true; - NOFIELDS?: true; - NOFREQS?: true; - SKIPINITIALSCAN?: true; - STOPWORDS?: string | Array; +import { RedisArgument, SimpleStringReply, Command, CommandArguments } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument, pushOptionalVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; + +export const SCHEMA_FIELD_TYPE = { + TEXT: 'TEXT', + NUMERIC: 'NUMERIC', + GEO: 'GEO', + TAG: 'TAG', + VECTOR: 'VECTOR', + GEOSHAPE: 'GEOSHAPE' +} as const; + +export type SchemaFieldType = typeof SCHEMA_FIELD_TYPE[keyof typeof SCHEMA_FIELD_TYPE]; + +interface SchemaField { + type: T; + AS?: RedisArgument; + INDEXMISSING?: boolean; +} + +interface SchemaCommonField extends SchemaField { + SORTABLE?: boolean | 'UNF' + NOINDEX?: boolean; +} + +export const SCHEMA_TEXT_FIELD_PHONETIC = { + DM_EN: 'dm:en', + DM_FR: 'dm:fr', + FM_PT: 'dm:pt', + DM_ES: 'dm:es' +} as const; + +export type SchemaTextFieldPhonetic = typeof SCHEMA_TEXT_FIELD_PHONETIC[keyof typeof SCHEMA_TEXT_FIELD_PHONETIC]; + +interface SchemaTextField extends SchemaCommonField { + NOSTEM?: boolean; + WEIGHT?: number; + PHONETIC?: SchemaTextFieldPhonetic; + WITHSUFFIXTRIE?: boolean; + INDEXEMPTY?: boolean; +} + +interface SchemaNumericField extends SchemaCommonField {} + +interface SchemaGeoField extends SchemaCommonField {} + +interface SchemaTagField extends SchemaCommonField { + SEPARATOR?: RedisArgument; + CASESENSITIVE?: boolean; + WITHSUFFIXTRIE?: boolean; + INDEXEMPTY?: boolean; +} + +export const SCHEMA_VECTOR_FIELD_ALGORITHM = { + FLAT: 'FLAT', + HNSW: 'HNSW' +} as const; + +export type SchemaVectorFieldAlgorithm = typeof SCHEMA_VECTOR_FIELD_ALGORITHM[keyof typeof SCHEMA_VECTOR_FIELD_ALGORITHM]; + +interface SchemaVectorField extends SchemaField { + ALGORITHM: SchemaVectorFieldAlgorithm; + TYPE: string; + DIM: number; + DISTANCE_METRIC: 'L2' | 'IP' | 'COSINE'; + INITIAL_CAP?: number; +} + +interface SchemaFlatVectorField extends SchemaVectorField { + ALGORITHM: typeof SCHEMA_VECTOR_FIELD_ALGORITHM['FLAT']; + BLOCK_SIZE?: number; } -export function transformArguments(index: string, schema: RediSearchSchema, options?: CreateOptions): Array { +interface SchemaHNSWVectorField extends SchemaVectorField { + ALGORITHM: typeof SCHEMA_VECTOR_FIELD_ALGORITHM['HNSW']; + M?: number; + EF_CONSTRUCTION?: number; + EF_RUNTIME?: number; +} + +export const SCHEMA_GEO_SHAPE_COORD_SYSTEM = { + SPHERICAL: 'SPHERICAL', + FLAT: 'FLAT' +} as const; + +export type SchemaGeoShapeFieldCoordSystem = typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM[keyof typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM]; + +interface SchemaGeoShapeField extends SchemaField { + COORD_SYSTEM?: SchemaGeoShapeFieldCoordSystem; +} + +export interface RediSearchSchema { + [field: string]: ( + SchemaTextField | + SchemaNumericField | + SchemaGeoField | + SchemaTagField | + SchemaFlatVectorField | + SchemaHNSWVectorField | + SchemaGeoShapeField | + SchemaFieldType + ); +} + +function pushCommonSchemaFieldOptions(args: CommandArguments, fieldOptions: SchemaCommonField) { + if (fieldOptions.SORTABLE) { + args.push('SORTABLE'); + + if (fieldOptions.SORTABLE === 'UNF') { + args.push('UNF'); + } + } + + if (fieldOptions.NOINDEX) { + args.push('NOINDEX'); + } +} + +export function pushSchema(args: CommandArguments, schema: RediSearchSchema) { + for (const [field, fieldOptions] of Object.entries(schema)) { + args.push(field); + + if (typeof fieldOptions === 'string') { + args.push(fieldOptions); + continue; + } + + if (fieldOptions.AS) { + args.push('AS', fieldOptions.AS); + } + + args.push(fieldOptions.type); + + if (fieldOptions.INDEXMISSING) { + args.push('INDEXMISSING'); + } + + switch (fieldOptions.type) { + case SCHEMA_FIELD_TYPE.TEXT: + if (fieldOptions.NOSTEM) { + args.push('NOSTEM'); + } + + if (fieldOptions.WEIGHT) { + args.push('WEIGHT', fieldOptions.WEIGHT.toString()); + } + + if (fieldOptions.PHONETIC) { + args.push('PHONETIC', fieldOptions.PHONETIC); + } + + if (fieldOptions.WITHSUFFIXTRIE) { + args.push('WITHSUFFIXTRIE'); + } + + if (fieldOptions.INDEXEMPTY) { + args.push('INDEXEMPTY'); + } + + pushCommonSchemaFieldOptions(args, fieldOptions) + break; + + case SCHEMA_FIELD_TYPE.NUMERIC: + case SCHEMA_FIELD_TYPE.GEO: + pushCommonSchemaFieldOptions(args, fieldOptions) + break; + + case SCHEMA_FIELD_TYPE.TAG: + if (fieldOptions.SEPARATOR) { + args.push('SEPARATOR', fieldOptions.SEPARATOR); + } + + if (fieldOptions.CASESENSITIVE) { + args.push('CASESENSITIVE'); + } + + if (fieldOptions.WITHSUFFIXTRIE) { + args.push('WITHSUFFIXTRIE'); + } + + if (fieldOptions.INDEXEMPTY) { + args.push('INDEXEMPTY'); + } + + pushCommonSchemaFieldOptions(args, fieldOptions) + break; + + case SCHEMA_FIELD_TYPE.VECTOR: + args.push(fieldOptions.ALGORITHM); + + const lengthIndex = args.push('') - 1; + + args.push( + 'TYPE', fieldOptions.TYPE, + 'DIM', fieldOptions.DIM.toString(), + 'DISTANCE_METRIC', fieldOptions.DISTANCE_METRIC + ); + + if (fieldOptions.INITIAL_CAP) { + args.push('INITIAL_CAP', fieldOptions.INITIAL_CAP.toString()); + } + + switch (fieldOptions.ALGORITHM) { + case SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT: + if (fieldOptions.BLOCK_SIZE) { + args.push('BLOCK_SIZE', fieldOptions.BLOCK_SIZE.toString()); + } + + break; + + case SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW: + if (fieldOptions.M) { + args.push('M', fieldOptions.M.toString()); + } + + if (fieldOptions.EF_CONSTRUCTION) { + args.push('EF_CONSTRUCTION', fieldOptions.EF_CONSTRUCTION.toString()); + } + + if (fieldOptions.EF_RUNTIME) { + args.push('EF_RUNTIME', fieldOptions.EF_RUNTIME.toString()); + } + + break; + } + args[lengthIndex] = (args.length - lengthIndex - 1).toString(); + + break; + + case SCHEMA_FIELD_TYPE.GEOSHAPE: + if (fieldOptions.COORD_SYSTEM !== undefined) { + args.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM); + } + + break; + } + } +} + +export const REDISEARCH_LANGUAGE = { + ARABIC: 'Arabic', + BASQUE: 'Basque', + CATALANA: 'Catalan', + DANISH: 'Danish', + DUTCH: 'Dutch', + ENGLISH: 'English', + FINNISH: 'Finnish', + FRENCH: 'French', + GERMAN: 'German', + GREEK: 'Greek', + HUNGARIAN: 'Hungarian', + INDONESAIN: 'Indonesian', + IRISH: 'Irish', + ITALIAN: 'Italian', + LITHUANIAN: 'Lithuanian', + NEPALI: 'Nepali', + NORWEIGAN: 'Norwegian', + PORTUGUESE: 'Portuguese', + ROMANIAN: 'Romanian', + RUSSIAN: 'Russian', + SPANISH: 'Spanish', + SWEDISH: 'Swedish', + TAMIL: 'Tamil', + TURKISH: 'Turkish', + CHINESE: 'Chinese' +} as const; + +export type RediSearchLanguage = typeof REDISEARCH_LANGUAGE[keyof typeof REDISEARCH_LANGUAGE]; + +export type RediSearchProperty = `${'@' | '$.'}${string}`; + +export interface CreateOptions { + ON?: 'HASH' | 'JSON'; + PREFIX?: RedisVariadicArgument; + FILTER?: RedisArgument; + LANGUAGE?: RediSearchLanguage; + LANGUAGE_FIELD?: RediSearchProperty; + SCORE?: number; + SCORE_FIELD?: RediSearchProperty; + // PAYLOAD_FIELD?: string; + MAXTEXTFIELDS?: boolean; + TEMPORARY?: number; + NOOFFSETS?: boolean; + NOHL?: boolean; + NOFIELDS?: boolean; + NOFREQS?: boolean; + SKIPINITIALSCAN?: boolean; + STOPWORDS?: RedisVariadicArgument; +} + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument, schema: RediSearchSchema, options?: CreateOptions) { const args = ['FT.CREATE', index]; if (options?.ON) { - args.push('ON', options.ON); + args.push('ON', options.ON); } - pushOptionalVerdictArgument(args, 'PREFIX', options?.PREFIX); + pushOptionalVariadicArgument(args, 'PREFIX', options?.PREFIX); if (options?.FILTER) { - args.push('FILTER', options.FILTER); + args.push('FILTER', options.FILTER); } if (options?.LANGUAGE) { - args.push('LANGUAGE', options.LANGUAGE); + args.push('LANGUAGE', options.LANGUAGE); } if (options?.LANGUAGE_FIELD) { - args.push('LANGUAGE_FIELD', options.LANGUAGE_FIELD); + args.push('LANGUAGE_FIELD', options.LANGUAGE_FIELD); } if (options?.SCORE) { - args.push('SCORE', options.SCORE.toString()); + args.push('SCORE', options.SCORE.toString()); } if (options?.SCORE_FIELD) { - args.push('SCORE_FIELD', options.SCORE_FIELD); + args.push('SCORE_FIELD', options.SCORE_FIELD); } // if (options?.PAYLOAD_FIELD) { @@ -54,38 +325,38 @@ export function transformArguments(index: string, schema: RediSearchSchema, opti // } if (options?.MAXTEXTFIELDS) { - args.push('MAXTEXTFIELDS'); + args.push('MAXTEXTFIELDS'); } if (options?.TEMPORARY) { - args.push('TEMPORARY', options.TEMPORARY.toString()); + args.push('TEMPORARY', options.TEMPORARY.toString()); } if (options?.NOOFFSETS) { - args.push('NOOFFSETS'); + args.push('NOOFFSETS'); } if (options?.NOHL) { - args.push('NOHL'); + args.push('NOHL'); } if (options?.NOFIELDS) { - args.push('NOFIELDS'); + args.push('NOFIELDS'); } if (options?.NOFREQS) { - args.push('NOFREQS'); + args.push('NOFREQS'); } if (options?.SKIPINITIALSCAN) { - args.push('SKIPINITIALSCAN'); + args.push('SKIPINITIALSCAN'); } - pushOptionalVerdictArgument(args, 'STOPWORDS', options?.STOPWORDS); + pushOptionalVariadicArgument(args, 'STOPWORDS', options?.STOPWORDS); args.push('SCHEMA'); pushSchema(args, schema); return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/search/lib/commands/CURSOR_DEL.spec.ts b/packages/search/lib/commands/CURSOR_DEL.spec.ts index d89725ef80d..8e9a7cf9aec 100644 --- a/packages/search/lib/commands/CURSOR_DEL.spec.ts +++ b/packages/search/lib/commands/CURSOR_DEL.spec.ts @@ -1,33 +1,32 @@ -import { strict as assert } from 'assert'; -import { SchemaFieldTypes } from '.'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CURSOR_DEL'; +import CURSOR_DEL from './CURSOR_DEL'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; -describe('CURSOR DEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('index', 0), - ['FT.CURSOR', 'DEL', 'index', '0'] - ); - }); +describe('FT.CURSOR DEL', () => { + it('transformArguments', () => { + assert.deepEqual( + CURSOR_DEL.transformArguments('index', 0), + ['FT.CURSOR', 'DEL', 'index', '0'] + ); + }); - testUtils.testWithClient('client.ft.cursorDel', async client => { - const [ ,, { cursor } ] = await Promise.all([ - client.ft.create('idx', { - field: { - type: SchemaFieldTypes.TEXT - } - }), - client.hSet('key', 'field', 'value'), - client.ft.aggregateWithCursor('idx', '*', { - COUNT: 1 - }) - ]); + testUtils.testWithClient('client.ft.cursorDel', async client => { + const [, , { cursor }] = await Promise.all([ + client.ft.create('idx', { + field: { + type: SCHEMA_FIELD_TYPE.TEXT + } + }), + client.hSet('key', 'field', 'value'), + client.ft.aggregateWithCursor('idx', '*', { + COUNT: 1 + }) + ]); - - assert.equal( - await client.ft.cursorDel('idx', cursor), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal( + await client.ft.cursorDel('idx', cursor), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/CURSOR_DEL.ts b/packages/search/lib/commands/CURSOR_DEL.ts index 22c850f2a89..afccd695ff3 100644 --- a/packages/search/lib/commands/CURSOR_DEL.ts +++ b/packages/search/lib/commands/CURSOR_DEL.ts @@ -1,14 +1,10 @@ -import { RedisCommandArgument } from '@redis/client/dist/lib/commands'; +import { SimpleStringReply, Command, RedisArgument, NumberReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(index: RedisCommandArgument, cursorId: number) { - return [ - 'FT.CURSOR', - 'DEL', - index, - cursorId.toString() - ]; -} - -export declare function transformReply(): 'OK'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument, cursorId: UnwrapReply) { + return ['FT.CURSOR', 'DEL', index, cursorId.toString()]; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/search/lib/commands/CURSOR_READ.spec.ts b/packages/search/lib/commands/CURSOR_READ.spec.ts index bb68e2b6396..5999d4a7c18 100644 --- a/packages/search/lib/commands/CURSOR_READ.spec.ts +++ b/packages/search/lib/commands/CURSOR_READ.spec.ts @@ -1,45 +1,44 @@ -import { strict as assert } from 'assert'; -import { SchemaFieldTypes } from '.'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CURSOR_READ'; +import CURSOR_READ from './CURSOR_READ'; -describe('CURSOR READ', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('index', 0), - ['FT.CURSOR', 'READ', 'index', '0'] - ); - }); +describe('FT.CURSOR READ', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + CURSOR_READ.transformArguments('index', 0), + ['FT.CURSOR', 'READ', 'index', '0'] + ); + }); - it('with COUNT', () => { - assert.deepEqual( - transformArguments('index', 0, { COUNT: 1 }), - ['FT.CURSOR', 'READ', 'index', '0', 'COUNT', '1'] - ); - }); + it('with COUNT', () => { + assert.deepEqual( + CURSOR_READ.transformArguments('index', 0, { + COUNT: 1 + }), + ['FT.CURSOR', 'READ', 'index', '0', 'COUNT', '1'] + ); }); + }); - testUtils.testWithClient('client.ft.cursorRead', async client => { - const [, , { cursor }] = await Promise.all([ - client.ft.create('idx', { - field: { - type: SchemaFieldTypes.TEXT - } - }), - client.hSet('key', 'field', 'value'), - client.ft.aggregateWithCursor('idx', '*', { - COUNT: 1 - }) - ]); + testUtils.testWithClient('client.ft.cursorRead', async client => { + const [, , { cursor }] = await Promise.all([ + client.ft.create('idx', { + field: 'TEXT' + }), + client.hSet('key', 'field', 'value'), + client.ft.aggregateWithCursor('idx', '*', { + COUNT: 1 + }) + ]); - assert.deepEqual( - await client.ft.cursorRead('idx', cursor), - { - total: 0, - results: [], - cursor: 0 - } - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual( + await client.ft.cursorRead('idx', cursor), + { + total: 0, + results: [], + cursor: 0 + } + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/CURSOR_READ.ts b/packages/search/lib/commands/CURSOR_READ.ts index 35cf1bc4f06..d08b22ba90d 100644 --- a/packages/search/lib/commands/CURSOR_READ.ts +++ b/packages/search/lib/commands/CURSOR_READ.ts @@ -1,30 +1,22 @@ -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { RedisArgument, Command, UnwrapReply, NumberReply } from '@redis/client/dist/lib/RESP/types'; +import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -interface CursorReadOptions { - COUNT?: number; +export interface FtCursorReadOptions { + COUNT?: number; } -export function transformArguments( - index: RedisCommandArgument, - cursor: number, - options?: CursorReadOptions -): RedisCommandArguments { - const args = [ - 'FT.CURSOR', - 'READ', - index, - cursor.toString() - ]; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument, cursor: UnwrapReply, options?: FtCursorReadOptions) { + const args = ['FT.CURSOR', 'READ', index, cursor.toString()]; - if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + if (options?.COUNT !== undefined) { + args.push('COUNT', options.COUNT.toString()); } return args; -} - -export { transformReply } from './AGGREGATE_WITHCURSOR'; + }, + transformReply: AGGREGATE_WITHCURSOR.transformReply, + unstableResp3: true +} as const satisfies Command; diff --git a/packages/search/lib/commands/DICTADD.spec.ts b/packages/search/lib/commands/DICTADD.spec.ts index b5f29dd4083..c18502ea4d0 100644 --- a/packages/search/lib/commands/DICTADD.spec.ts +++ b/packages/search/lib/commands/DICTADD.spec.ts @@ -1,28 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DICTADD'; +import DICTADD from './DICTADD'; -describe('DICTADD', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('dictionary', 'term'), - ['FT.DICTADD', 'dictionary', 'term'] - ); - }); +describe('FT.DICTADD', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + DICTADD.transformArguments('dictionary', 'term'), + ['FT.DICTADD', 'dictionary', 'term'] + ); + }); - it('Array', () => { - assert.deepEqual( - transformArguments('dictionary', ['1', '2']), - ['FT.DICTADD', 'dictionary', '1', '2'] - ); - }); + it('Array', () => { + assert.deepEqual( + DICTADD.transformArguments('dictionary', ['1', '2']), + ['FT.DICTADD', 'dictionary', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.ft.dictAdd', async client => { - assert.equal( - await client.ft.dictAdd('dictionary', 'term'), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ft.dictAdd', async client => { + assert.equal( + await client.ft.dictAdd('dictionary', 'term'), + 1 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/DICTADD.ts b/packages/search/lib/commands/DICTADD.ts index 60af11fd41f..f633d58b1f3 100644 --- a/packages/search/lib/commands/DICTADD.ts +++ b/packages/search/lib/commands/DICTADD.ts @@ -1,8 +1,11 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { pushVariadicArguments, RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -export function transformArguments(dictionary: string, term: string | Array): RedisCommandArguments { - return pushVerdictArguments(['FT.DICTADD', dictionary], term); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(dictionary: RedisArgument, term: RedisVariadicArgument) { + return pushVariadicArguments(['FT.DICTADD', dictionary], term); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/search/lib/commands/DICTDEL.spec.ts b/packages/search/lib/commands/DICTDEL.spec.ts index 5ffa6b6b84f..a7ca1b35cd3 100644 --- a/packages/search/lib/commands/DICTDEL.spec.ts +++ b/packages/search/lib/commands/DICTDEL.spec.ts @@ -1,28 +1,28 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DICTDEL'; +import DICTDEL from './DICTDEL'; -describe('DICTDEL', () => { - describe('transformArguments', () => { - it('string', () => { - assert.deepEqual( - transformArguments('dictionary', 'term'), - ['FT.DICTDEL', 'dictionary', 'term'] - ); - }); +describe('FT.DICTDEL', () => { + describe('transformArguments', () => { + it('string', () => { + assert.deepEqual( + DICTDEL.transformArguments('dictionary', 'term'), + ['FT.DICTDEL', 'dictionary', 'term'] + ); + }); - it('Array', () => { - assert.deepEqual( - transformArguments('dictionary', ['1', '2']), - ['FT.DICTDEL', 'dictionary', '1', '2'] - ); - }); + it('Array', () => { + assert.deepEqual( + DICTDEL.transformArguments('dictionary', ['1', '2']), + ['FT.DICTDEL', 'dictionary', '1', '2'] + ); }); + }); - testUtils.testWithClient('client.ft.dictDel', async client => { - assert.equal( - await client.ft.dictDel('dictionary', 'term'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ft.dictDel', async client => { + assert.equal( + await client.ft.dictDel('dictionary', 'term'), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/DICTDEL.ts b/packages/search/lib/commands/DICTDEL.ts index a1b728f1926..087211751ee 100644 --- a/packages/search/lib/commands/DICTDEL.ts +++ b/packages/search/lib/commands/DICTDEL.ts @@ -1,8 +1,11 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { pushVariadicArguments, RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -export function transformArguments(dictionary: string, term: string | Array): RedisCommandArguments { - return pushVerdictArguments(['FT.DICTDEL', dictionary], term); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(dictionary: RedisArgument, term: RedisVariadicArgument) { + return pushVariadicArguments(['FT.DICTDEL', dictionary], term); + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/search/lib/commands/DICTDUMP.spec.ts b/packages/search/lib/commands/DICTDUMP.spec.ts index 9896fb9440d..fe8e9441189 100644 --- a/packages/search/lib/commands/DICTDUMP.spec.ts +++ b/packages/search/lib/commands/DICTDUMP.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DICTDUMP'; +import DICTDUMP from './DICTDUMP'; -describe('DICTDUMP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('dictionary'), - ['FT.DICTDUMP', 'dictionary'] - ); - }); +describe('FT.DICTDUMP', () => { + it('transformArguments', () => { + assert.deepEqual( + DICTDUMP.transformArguments('dictionary'), + ['FT.DICTDUMP', 'dictionary'] + ); + }); - testUtils.testWithClient('client.ft.dictDump', async client => { - await client.ft.dictAdd('dictionary', 'string') + testUtils.testWithClient('client.ft.dictDump', async client => { + const [, reply] = await Promise.all([ + client.ft.dictAdd('dictionary', 'string'), + client.ft.dictDump('dictionary') + ]); - assert.deepEqual( - await client.ft.dictDump('dictionary'), - ['string'] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, ['string']); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/DICTDUMP.ts b/packages/search/lib/commands/DICTDUMP.ts index 1427bb42cb7..f542403cc57 100644 --- a/packages/search/lib/commands/DICTDUMP.ts +++ b/packages/search/lib/commands/DICTDUMP.ts @@ -1,5 +1,13 @@ -export function transformArguments(dictionary: string): Array { - return ['FT.DICTDUMP', dictionary]; -} +import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(dictionary: RedisArgument) { + return ['FT.DICTDUMP', dictionary]; + }, + transformReply: { + 2: undefined as unknown as () => ArrayReply, + 3: undefined as unknown as () => SetReply + } +} as const satisfies Command; diff --git a/packages/search/lib/commands/DROPINDEX.spec.ts b/packages/search/lib/commands/DROPINDEX.spec.ts index 6a60a5d851f..5fcbaca08ce 100644 --- a/packages/search/lib/commands/DROPINDEX.spec.ts +++ b/packages/search/lib/commands/DROPINDEX.spec.ts @@ -1,33 +1,33 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { SchemaFieldTypes } from '.'; -import { transformArguments } from './DROPINDEX'; +import DROPINDEX from './DROPINDEX'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; -describe('DROPINDEX', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('index'), - ['FT.DROPINDEX', 'index'] - ); - }); +describe('FT.DROPINDEX', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + DROPINDEX.transformArguments('index'), + ['FT.DROPINDEX', 'index'] + ); + }); - it('with DD', () => { - assert.deepEqual( - transformArguments('index', { DD: true }), - ['FT.DROPINDEX', 'index', 'DD'] - ); - }); + it('with DD', () => { + assert.deepEqual( + DROPINDEX.transformArguments('index', { DD: true }), + ['FT.DROPINDEX', 'index', 'DD'] + ); }); + }); - testUtils.testWithClient('client.ft.dropIndex', async client => { - await client.ft.create('index', { - field: SchemaFieldTypes.TEXT - }); + testUtils.testWithClient('client.ft.dropIndex', async client => { + const [, reply] = await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }), + client.ft.dropIndex('index') + ]); - assert.equal( - await client.ft.dropIndex('index'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/DROPINDEX.ts b/packages/search/lib/commands/DROPINDEX.ts index 7897a9dd82e..64fe9711e7f 100644 --- a/packages/search/lib/commands/DROPINDEX.ts +++ b/packages/search/lib/commands/DROPINDEX.ts @@ -1,15 +1,23 @@ -interface DropIndexOptions { - DD?: true; +import { RedisArgument, SimpleStringReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; + +export interface FtDropIndexOptions { + DD?: true; } -export function transformArguments(index: string, options?: DropIndexOptions): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument, options?: FtDropIndexOptions) { const args = ['FT.DROPINDEX', index]; if (options?.DD) { - args.push('DD'); + args.push('DD'); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: { + 2: undefined as unknown as () => SimpleStringReply<'OK'>, + 3: undefined as unknown as () => NumberReply + } +} as const satisfies Command; diff --git a/packages/search/lib/commands/EXPLAIN.spec.ts b/packages/search/lib/commands/EXPLAIN.spec.ts index d24f5fe4ac5..e8b3555957f 100644 --- a/packages/search/lib/commands/EXPLAIN.spec.ts +++ b/packages/search/lib/commands/EXPLAIN.spec.ts @@ -1,33 +1,46 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './EXPLAIN'; +import { strict as assert } from 'node:assert'; +import EXPLAIN from './EXPLAIN'; +import testUtils, { GLOBAL } from '../test-utils'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; describe('EXPLAIN', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - transformArguments('index', '*'), - ['FT.EXPLAIN', 'index', '*'] - ); - }); + describe('transformArguments', () => { + it('simple', () => { + assert.deepEqual( + EXPLAIN.transformArguments('index', '*'), + ['FT.EXPLAIN', 'index', '*'] + ); + }); - it('with PARAMS', () => { - assert.deepEqual( - transformArguments('index', '*', { - PARAMS: { - param: 'value' - } - }), - ['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value'] - ); - }); + it('with PARAMS', () => { + assert.deepEqual( + EXPLAIN.transformArguments('index', '*', { + PARAMS: { + param: 'value' + } + }), + ['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value'] + ); + }); - it('with DIALECT', () => { - assert.deepEqual( - transformArguments('index', '*', { - DIALECT: 1 - }), - ['FT.EXPLAIN', 'index', '*', 'DIALECT', '1'] - ); - }); + it('with DIALECT', () => { + assert.deepEqual( + EXPLAIN.transformArguments('index', '*', { + DIALECT: 1 + }), + ['FT.EXPLAIN', 'index', '*', 'DIALECT', '1'] + ); }); + }); + + testUtils.testWithClient('client.ft.dropIndex', async client => { + const [, reply] = await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }), + client.ft.explain('index', '*') + ]); + + assert.equal(reply, '\n'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/EXPLAIN.ts b/packages/search/lib/commands/EXPLAIN.ts index ab3935ff979..0ad84feb68d 100644 --- a/packages/search/lib/commands/EXPLAIN.ts +++ b/packages/search/lib/commands/EXPLAIN.ts @@ -1,26 +1,28 @@ -import { Params, pushParamsArgs } from "."; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { FtSearchParams, pushParamsArgument } from './SEARCH'; -export const IS_READ_ONLY = true; - -interface ExplainOptions { - PARAMS?: Params; - DIALECT?: number; +export interface FtExplainOptions { + PARAMS?: FtSearchParams; + DIALECT?: number; } -export function transformArguments( - index: string, - query: string, - options?: ExplainOptions -): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments( + index: RedisArgument, + query: RedisArgument, + options?: FtExplainOptions + ) { const args = ['FT.EXPLAIN', index, query]; - pushParamsArgs(args, options?.PARAMS); + pushParamsArgument(args, options?.PARAMS); if (options?.DIALECT) { - args.push('DIALECT', options.DIALECT.toString()); + args.push('DIALECT', options.DIALECT.toString()); } return args; -} - -export declare function transformReply(): string; + }, + transformReply: undefined as unknown as () => SimpleStringReply +} as const satisfies Command; diff --git a/packages/search/lib/commands/EXPLAINCLI.spec.ts b/packages/search/lib/commands/EXPLAINCLI.spec.ts index 238ef44eaaa..3bffcf5fe5b 100644 --- a/packages/search/lib/commands/EXPLAINCLI.spec.ts +++ b/packages/search/lib/commands/EXPLAINCLI.spec.ts @@ -1,11 +1,11 @@ -import { strict as assert } from 'assert'; -import { transformArguments } from './EXPLAINCLI'; +import { strict as assert } from 'node:assert'; +import EXPLAINCLI from './EXPLAINCLI'; describe('EXPLAINCLI', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('index', '*'), - ['FT.EXPLAINCLI', 'index', '*'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + EXPLAINCLI.transformArguments('index', '*'), + ['FT.EXPLAINCLI', 'index', '*'] + ); + }); }); diff --git a/packages/search/lib/commands/EXPLAINCLI.ts b/packages/search/lib/commands/EXPLAINCLI.ts index db97fb9c8da..e16866991b9 100644 --- a/packages/search/lib/commands/EXPLAINCLI.ts +++ b/packages/search/lib/commands/EXPLAINCLI.ts @@ -1,7 +1,10 @@ -export const IS_READ_ONLY = true; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(index: string, query: string): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument, query: RedisArgument) { return ['FT.EXPLAINCLI', index, query]; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/search/lib/commands/INFO.spec.ts b/packages/search/lib/commands/INFO.spec.ts index e026b44e264..e7c7c897a84 100644 --- a/packages/search/lib/commands/INFO.spec.ts +++ b/packages/search/lib/commands/INFO.spec.ts @@ -1,26 +1,28 @@ -import { strict as assert } from 'assert'; -import { SchemaFieldTypes } from '.'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './INFO'; +import INFO, { InfoReply } from './INFO'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; describe('INFO', () => { it('transformArguments', () => { assert.deepEqual( - transformArguments('index'), + INFO.transformArguments('index'), ['FT.INFO', 'index'] ); }); testUtils.testWithClient('client.ft.info', async client => { await client.ft.create('index', { - field: SchemaFieldTypes.TEXT + field: SCHEMA_FIELD_TYPE.TEXT }); + const ret = await client.ft.info('index'); + // effectively testing that stopwords_list is not in ret assert.deepEqual( - await client.ft.info('index'), + ret, { - indexName: 'index', - indexOptions: [], - indexDefinition: Object.create(null, { + index_name: 'index', + index_options: [], + index_definition: Object.create(null, { default_score: { value: '1', configurable: true, @@ -59,41 +61,48 @@ describe('INFO', () => { enumerable: true } })], - numDocs: '0', - maxDocId: '0', - numTerms: '0', - numRecords: '0', - invertedSzMb: '0', - vectorIndexSzMb: '0', - totalInvertedIndexBlocks: '0', - offsetVectorsSzMb: '0', - docTableSizeMb: '0', - sortableValuesSizeMb: '0', - keyTableSizeMb: '0', - recordsPerDocAvg: '-nan', - bytesPerRecordAvg: '-nan', - offsetsPerTermAvg: '-nan', - offsetBitsPerRecordAvg: '-nan', - hashIndexingFailures: '0', - indexing: '0', - percentIndexed: '1', - gcStats: { - bytesCollected: '0', - totalMsRun: '0', - totalCycles: '0', - averageCycleTimeMs: '-nan', - lastRunTimeMs: '0', - gcNumericTreesMissed: '0', - gcBlocksDenied: '0' + num_docs: 0, + max_doc_id: 0, + num_terms: 0, + num_records: 0, + inverted_sz_mb: 0, + vector_index_sz_mb: 0, + total_inverted_index_blocks: 0, + offset_vectors_sz_mb: 0, + doc_table_size_mb: 0, + sortable_values_size_mb: 0, + key_table_size_mb: 0, + records_per_doc_avg: NaN, + bytes_per_record_avg: NaN, + cleaning: 0, + offsets_per_term_avg: NaN, + offset_bits_per_record_avg: NaN, + geoshapes_sz_mb: 0, + hash_indexing_failures: 0, + indexing: 0, + percent_indexed: 1, + number_of_uses: 1, + tag_overhead_sz_mb: 0, + text_overhead_sz_mb: 0, + total_index_memory_sz_mb: 0, + total_indexing_time: 0, + gc_stats: { + bytes_collected: 0, + total_ms_run: 0, + total_cycles: 0, + average_cycle_time_ms: NaN, + last_run_time_ms: 0, + gc_numeric_trees_missed: 0, + gc_blocks_denied: 0 }, - cursorStats: { - globalIdle: 0, - globalTotal: 0, - indexCapacity: 128, - idnexTotal: 0 + cursor_stats: { + global_idle: 0, + global_total: 0, + index_capacity: 128, + index_total: 0 }, - stopWords: undefined } ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/INFO.ts b/packages/search/lib/commands/INFO.ts index 269d12d51cf..52b87769cef 100644 --- a/packages/search/lib/commands/INFO.ts +++ b/packages/search/lib/commands/INFO.ts @@ -1,167 +1,163 @@ -import { RedisCommandArgument } from '@redis/client/dist/lib/commands'; -import { transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { RedisArgument } from "@redis/client"; +import { ArrayReply, BlobStringReply, Command, DoubleReply, MapReply, NullReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/dist/lib/RESP/types"; +import { createTransformTuplesReplyFunc, transformDoubleReply } from "@redis/client/dist/lib/commands/generic-transformers"; +import { TuplesReply } from '@redis/client/lib/RESP/types'; -export function transformArguments(index: string): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument) { return ['FT.INFO', index]; + }, + transformReply: { + 2: transformV2Reply, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true +} as const satisfies Command; + +export interface InfoReply { + index_name: SimpleStringReply; + index_options: ArrayReply; + index_definition: MapReply; + attributes: Array>; + num_docs: NumberReply + max_doc_id: NumberReply; + num_terms: NumberReply; + num_records: NumberReply; + inverted_sz_mb: DoubleReply; + vector_index_sz_mb: DoubleReply; + total_inverted_index_blocks: NumberReply; + offset_vectors_sz_mb: DoubleReply; + doc_table_size_mb: DoubleReply; + sortable_values_size_mb: DoubleReply; + key_table_size_mb: DoubleReply; + tag_overhead_sz_mb: DoubleReply; + text_overhead_sz_mb: DoubleReply; + total_index_memory_sz_mb: DoubleReply; + geoshapes_sz_mb: DoubleReply; + records_per_doc_avg: DoubleReply; + bytes_per_record_avg: DoubleReply; + offsets_per_term_avg: DoubleReply; + offset_bits_per_record_avg: DoubleReply; + hash_indexing_failures: NumberReply; + total_indexing_time: DoubleReply; + indexing: NumberReply; + percent_indexed: DoubleReply; + number_of_uses: NumberReply; + cleaning: NumberReply; + gc_stats: { + bytes_collected: DoubleReply; + total_ms_run: DoubleReply; + total_cycles: DoubleReply; + average_cycle_time_ms: DoubleReply; + last_run_time_ms: DoubleReply; + gc_numeric_trees_missed: DoubleReply; + gc_blocks_denied: DoubleReply; + }; + cursor_stats: { + global_idle: NumberReply; + global_total: NumberReply; + index_capacity: NumberReply; + index_total: NumberReply; + }; + stopwords_list?: ArrayReply | TuplesReply<[NullReply]>; } -type InfoRawReply = [ - 'index_name', - RedisCommandArgument, - 'index_options', - Array, - 'index_definition', - Array, - 'attributes', - Array>, - 'num_docs', - RedisCommandArgument, - 'max_doc_id', - RedisCommandArgument, - 'num_terms', - RedisCommandArgument, - 'num_records', - RedisCommandArgument, - 'inverted_sz_mb', - RedisCommandArgument, - 'vector_index_sz_mb', - RedisCommandArgument, - 'total_inverted_index_blocks', - RedisCommandArgument, - 'offset_vectors_sz_mb', - RedisCommandArgument, - 'doc_table_size_mb', - RedisCommandArgument, - 'sortable_values_size_mb', - RedisCommandArgument, - 'key_table_size_mb', - RedisCommandArgument, - 'records_per_doc_avg', - RedisCommandArgument, - 'bytes_per_record_avg', - RedisCommandArgument, - 'offsets_per_term_avg', - RedisCommandArgument, - 'offset_bits_per_record_avg', - RedisCommandArgument, - 'hash_indexing_failures', - RedisCommandArgument, - 'indexing', - RedisCommandArgument, - 'percent_indexed', - RedisCommandArgument, - 'gc_stats', - [ - 'bytes_collected', - RedisCommandArgument, - 'total_ms_run', - RedisCommandArgument, - 'total_cycles', - RedisCommandArgument, - 'average_cycle_time_ms', - RedisCommandArgument, - 'last_run_time_ms', - RedisCommandArgument, - 'gc_numeric_trees_missed', - RedisCommandArgument, - 'gc_blocks_denied', - RedisCommandArgument - ], - 'cursor_stats', - [ - 'global_idle', - number, - 'global_total', - number, - 'index_capacity', - number, - 'index_total', - number - ], - 'stopwords_list'?, - Array? -]; +function transformV2Reply(reply: Array, preserve?: any, typeMapping?: TypeMapping): InfoReply { + const myTransformFunc = createTransformTuplesReplyFunc(preserve, typeMapping); -interface InfoReply { - indexName: RedisCommandArgument; - indexOptions: Array; - indexDefinition: Record; - attributes: Array>; - numDocs: RedisCommandArgument; - maxDocId: RedisCommandArgument; - numTerms: RedisCommandArgument; - numRecords: RedisCommandArgument; - invertedSzMb: RedisCommandArgument; - vectorIndexSzMb: RedisCommandArgument; - totalInvertedIndexBlocks: RedisCommandArgument; - offsetVectorsSzMb: RedisCommandArgument; - docTableSizeMb: RedisCommandArgument; - sortableValuesSizeMb: RedisCommandArgument; - keyTableSizeMb: RedisCommandArgument; - recordsPerDocAvg: RedisCommandArgument; - bytesPerRecordAvg: RedisCommandArgument; - offsetsPerTermAvg: RedisCommandArgument; - offsetBitsPerRecordAvg: RedisCommandArgument; - hashIndexingFailures: RedisCommandArgument; - indexing: RedisCommandArgument; - percentIndexed: RedisCommandArgument; - gcStats: { - bytesCollected: RedisCommandArgument; - totalMsRun: RedisCommandArgument; - totalCycles: RedisCommandArgument; - averageCycleTimeMs: RedisCommandArgument; - lastRunTimeMs: RedisCommandArgument; - gcNumericTreesMissed: RedisCommandArgument; - gcBlocksDenied: RedisCommandArgument; - }; - cursorStats: { - globalIdle: number; - globalTotal: number; - indexCapacity: number; - idnexTotal: number; - }; - stopWords: Array | undefined; -} + const ret = {} as unknown as InfoReply; + + for (let i=0; i < reply.length; i += 2) { + const key = reply[i].toString() as keyof InfoReply; + + switch (key) { + case 'index_name': + case 'index_options': + case 'num_docs': + case 'max_doc_id': + case 'num_terms': + case 'num_records': + case 'total_inverted_index_blocks': + case 'hash_indexing_failures': + case 'indexing': + case 'number_of_uses': + case 'cleaning': + case 'stopwords_list': + ret[key] = reply[i+1]; + break; + case 'inverted_sz_mb': + case 'vector_index_sz_mb': + case 'offset_vectors_sz_mb': + case 'doc_table_size_mb': + case 'sortable_values_size_mb': + case 'key_table_size_mb': + case 'text_overhead_sz_mb': + case 'tag_overhead_sz_mb': + case 'total_index_memory_sz_mb': + case 'geoshapes_sz_mb': + case 'records_per_doc_avg': + case 'bytes_per_record_avg': + case 'offsets_per_term_avg': + case 'offset_bits_per_record_avg': + case 'total_indexing_time': + case 'percent_indexed': + ret[key] = transformDoubleReply[2](reply[i+1], undefined, typeMapping) as DoubleReply; + break; + case 'index_definition': + ret[key] = myTransformFunc(reply[i+1]); + break; + case 'attributes': + ret[key] = (reply[i+1] as Array>).map(attribute => myTransformFunc(attribute)); + break; + case 'gc_stats': { + const innerRet = {} as unknown as InfoReply['gc_stats']; + + const array = reply[i+1]; + + for (let i=0; i < array.length; i += 2) { + const innerKey = array[i].toString() as keyof InfoReply['gc_stats']; + + switch (innerKey) { + case 'bytes_collected': + case 'total_ms_run': + case 'total_cycles': + case 'average_cycle_time_ms': + case 'last_run_time_ms': + case 'gc_numeric_trees_missed': + case 'gc_blocks_denied': + innerRet[innerKey] = transformDoubleReply[2](array[i+1], undefined, typeMapping) as DoubleReply; + break; + } + } + + ret[key] = innerRet; + break; + } + case 'cursor_stats': { + const innerRet = {} as unknown as InfoReply['cursor_stats']; + + const array = reply[i+1]; + + for (let i=0; i < array.length; i += 2) { + const innerKey = array[i].toString() as keyof InfoReply['cursor_stats']; + + switch (innerKey) { + case 'global_idle': + case 'global_total': + case 'index_capacity': + case 'index_total': + innerRet[innerKey] = array[i+1]; + break; + } + } + + ret[key] = innerRet; + break; + } + } + } -export function transformReply(rawReply: InfoRawReply): InfoReply { - return { - indexName: rawReply[1], - indexOptions: rawReply[3], - indexDefinition: transformTuplesReply(rawReply[5]), - attributes: rawReply[7].map(attribute => transformTuplesReply(attribute)), - numDocs: rawReply[9], - maxDocId: rawReply[11], - numTerms: rawReply[13], - numRecords: rawReply[15], - invertedSzMb: rawReply[17], - vectorIndexSzMb: rawReply[19], - totalInvertedIndexBlocks: rawReply[21], - offsetVectorsSzMb: rawReply[23], - docTableSizeMb: rawReply[25], - sortableValuesSizeMb: rawReply[27], - keyTableSizeMb: rawReply[29], - recordsPerDocAvg: rawReply[31], - bytesPerRecordAvg: rawReply[33], - offsetsPerTermAvg: rawReply[35], - offsetBitsPerRecordAvg: rawReply[37], - hashIndexingFailures: rawReply[39], - indexing: rawReply[41], - percentIndexed: rawReply[43], - gcStats: { - bytesCollected: rawReply[45][1], - totalMsRun: rawReply[45][3], - totalCycles: rawReply[45][5], - averageCycleTimeMs: rawReply[45][7], - lastRunTimeMs: rawReply[45][9], - gcNumericTreesMissed: rawReply[45][11], - gcBlocksDenied: rawReply[45][13] - }, - cursorStats: { - globalIdle: rawReply[47][1], - globalTotal: rawReply[47][3], - indexCapacity: rawReply[47][5], - idnexTotal: rawReply[47][7] - }, - stopWords: rawReply[49] - }; + return ret; } diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts index c3d6f990ff7..8644ca5201e 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts @@ -1,25 +1,25 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { SchemaFieldTypes } from '.'; -import { transformArguments } from './PROFILE_AGGREGATE'; -import { AggregateSteps } from './AGGREGATE'; +import { FT_AGGREGATE_STEPS } from './AGGREGATE'; +import PROFILE_AGGREGATE from './PROFILE_AGGREGATE'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; describe('PROFILE AGGREGATE', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - transformArguments('index', 'query'), + PROFILE_AGGREGATE.transformArguments('index', 'query'), ['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query'] ); }); it('with options', () => { assert.deepEqual( - transformArguments('index', 'query', { + PROFILE_AGGREGATE.transformArguments('index', 'query', { LIMITED: true, VERBATIM: true, STEPS: [{ - type: AggregateSteps.SORTBY, + type: FT_AGGREGATE_STEPS.SORTBY, BY: '@by' }] }), @@ -32,13 +32,14 @@ describe('PROFILE AGGREGATE', () => { testUtils.testWithClient('client.ft.search', async client => { await Promise.all([ client.ft.create('index', { - field: SchemaFieldTypes.NUMERIC + field: SCHEMA_FIELD_TYPE.NUMERIC }), client.hSet('1', 'field', '1'), client.hSet('2', 'field', '2') ]); const res = await client.ft.profileAggregate('index', '*'); + assert.deepEqual('None', res.profile.warning); assert.ok(typeof res.profile.iteratorsProfile.counter === 'number'); assert.ok(typeof res.profile.parsingTime === 'string'); assert.ok(res.results.total == 1); diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.ts index b28e06ade91..b6a8db38665 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.ts @@ -1,29 +1,38 @@ -import { pushAggregatehOptions, AggregateOptions, transformReply as transformAggregateReply, AggregateRawReply } from './AGGREGATE'; -import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.'; +// import { pushAggregatehOptions, AggregateOptions, transformReply as transformAggregateReply, AggregateRawReply } from './AGGREGATE'; +// import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.'; -export const IS_READ_ONLY = true; +import { Command, ReplyUnion } from "@redis/client/dist/lib/RESP/types"; +import AGGREGATE, { AggregateRawReply, FtAggregateOptions, pushAggregateOptions } from "./AGGREGATE"; +import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from "./PROFILE_SEARCH"; -export function transformArguments( - index: string, - query: string, - options?: ProfileOptions & AggregateOptions -): Array { - const args = ['FT.PROFILE', index, 'AGGREGATE']; - - if (options?.LIMITED) { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments( + index: string, + query: string, + options?: ProfileOptions & FtAggregateOptions + ) { + const args = ['FT.PROFILE', index, 'AGGREGATE']; + + if (options?.LIMITED) { args.push('LIMITED'); - } - - args.push('QUERY', query); - pushAggregatehOptions(args, options) - return args; -} + } + + args.push('QUERY', query); -type ProfileAggeregateRawReply = ProfileRawReply; + return pushAggregateOptions(args, options) + }, + transformReply: { + 2: (reply: ProfileAggeregateRawReply): ProfileReply => { + return { + results: AGGREGATE.transformReply[2](reply[0]), + profile: transformProfile(reply[1]) + } + }, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true + } as const satisfies Command; -export function transformReply(reply: ProfileAggeregateRawReply): ProfileReply { - return { - results: transformAggregateReply(reply[0]), - profile: transformProfile(reply[1]) - }; -} + type ProfileAggeregateRawReply = ProfileRawReply; \ No newline at end of file diff --git a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts index 6d7c5adda1e..a6e2a968d43 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts @@ -1,20 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { SchemaFieldTypes } from '.'; -import { transformArguments } from './PROFILE_SEARCH'; +import PROFILE_SEARCH from './PROFILE_SEARCH'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; + describe('PROFILE SEARCH', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - transformArguments('index', 'query'), + PROFILE_SEARCH.transformArguments('index', 'query'), ['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query'] ); }); it('with options', () => { assert.deepEqual( - transformArguments('index', 'query', { + PROFILE_SEARCH.transformArguments('index', 'query', { LIMITED: true, VERBATIM: true, INKEYS: 'key' @@ -28,12 +29,13 @@ describe('PROFILE SEARCH', () => { testUtils.testWithClient('client.ft.search', async client => { await Promise.all([ client.ft.create('index', { - field: SchemaFieldTypes.NUMERIC + field: SCHEMA_FIELD_TYPE.NUMERIC }), client.hSet('1', 'field', '1') ]); const res = await client.ft.profileSearch('index', '*'); + assert.strictEqual('None', res.profile.warning); assert.ok(typeof res.profile.iteratorsProfile.counter === 'number'); assert.ok(typeof res.profile.parsingTime === 'string'); assert.ok(res.results.total == 1); diff --git a/packages/search/lib/commands/PROFILE_SEARCH.ts b/packages/search/lib/commands/PROFILE_SEARCH.ts index 94fba8a6a54..5b9e918083b 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.ts @@ -1,29 +1,152 @@ -import { SearchOptions, SearchRawReply, transformReply as transformSearchReply } from './SEARCH'; -import { pushSearchOptions, ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +// import { SearchOptions, SearchRawReply, transformReply as transformSearchReply } from './SEARCH'; +// import { pushSearchOptions, ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.'; +// import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -export const IS_READ_ONLY = true; +import { Command, RedisArgument, ReplyUnion } from "@redis/client/dist/lib/RESP/types"; +import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, pushSearchOptions } from "./SEARCH"; +import { AggregateReply } from "./AGGREGATE"; -export function transformArguments( - index: string, - query: string, - options?: ProfileOptions & SearchOptions -): RedisCommandArguments { - let args: RedisCommandArguments = ['FT.PROFILE', index, 'SEARCH']; +export type ProfileRawReply = [ + results: T, + profile: [ + _: string, + TotalProfileTime: string, + _: string, + ParsingTime: string, + _: string, + PipelineCreationTime: string, + _: string, + IteratorsProfile: Array + ] +]; + +type ProfileSearchRawReply = ProfileRawReply; + +export interface ProfileOptions { + LIMITED?: true; +} + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments( + index: RedisArgument, + query: RedisArgument, + options?: ProfileOptions & FtSearchOptions + ) { + let args: Array = ['FT.PROFILE', index, 'SEARCH']; if (options?.LIMITED) { - args.push('LIMITED'); + args.push('LIMITED'); } args.push('QUERY', query); + return pushSearchOptions(args, options); + }, + transformReply: { + 2: (reply: ProfileSearchRawReply, withoutDocuments: boolean): ProfileReply => { + return { + results: SEARCH.transformReply[2](reply[0]), + profile: transformProfile(reply[1]) + } + }, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true +} as const satisfies Command; + +export interface ProfileReply { + results: SearchReply | AggregateReply; + profile: ProfileData; } -type ProfileSearchRawReply = ProfileRawReply; +interface ChildIterator { + type?: string, + counter?: number, + term?: string, + size?: number, + time?: string, + childIterators?: Array +} -export function transformReply(reply: ProfileSearchRawReply, withoutDocuments: boolean): ProfileReply { - return { - results: transformSearchReply(reply[0], withoutDocuments), - profile: transformProfile(reply[1]) - }; +interface IteratorsProfile { + type?: string, + counter?: number, + queryType?: string, + time?: string, + childIterators?: Array } + +interface ProfileData { + totalProfileTime: string, + parsingTime: string, + pipelineCreationTime: string, + warning: string, + iteratorsProfile: IteratorsProfile +} + +export function transformProfile(reply: Array): ProfileData{ + return { + totalProfileTime: reply[0][1], + parsingTime: reply[1][1], + pipelineCreationTime: reply[2][1], + warning: reply[3][1] ? reply[3][1] : 'None', + iteratorsProfile: transformIterators(reply[4][1]) + }; +} + +function transformIterators(IteratorsProfile: Array): IteratorsProfile { + var res: IteratorsProfile = {}; + for (let i = 0; i < IteratorsProfile.length; i += 2) { + const value = IteratorsProfile[i+1]; + switch (IteratorsProfile[i]) { + case 'Type': + res.type = value; + break; + case 'Counter': + res.counter = value; + break; + case 'Time': + res.time = value; + break; + case 'Query type': + res.queryType = value; + break; + case 'Child iterators': + res.childIterators = value.map(transformChildIterators); + break; + } + } + + return res; +} + +function transformChildIterators(IteratorsProfile: Array): ChildIterator { + var res: ChildIterator = {}; + for (let i = 1; i < IteratorsProfile.length; i += 2) { + const value = IteratorsProfile[i+1]; + switch (IteratorsProfile[i]) { + case 'Type': + res.type = value; + break; + case 'Counter': + res.counter = value; + break; + case 'Time': + res.time = value; + break; + case 'Size': + res.size = value; + break; + case 'Term': + res.term = value; + break; + case 'Child iterators': + res.childIterators = value.map(transformChildIterators); + break; + } + } + + return res; +} \ No newline at end of file diff --git a/packages/search/lib/commands/SEARCH.spec.ts b/packages/search/lib/commands/SEARCH.spec.ts index 931458b3a25..257dbb79515 100644 --- a/packages/search/lib/commands/SEARCH.spec.ts +++ b/packages/search/lib/commands/SEARCH.spec.ts @@ -1,300 +1,327 @@ -import { strict as assert } from 'assert'; -import { RedisSearchLanguages, SchemaFieldTypes } from '.'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SEARCH'; +import SEARCH from './SEARCH'; -describe('SEARCH', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('index', 'query'), - ['FT.SEARCH', 'index', 'query'] - ); - }); - - it('with VERBATIM', () => { - assert.deepEqual( - transformArguments('index', 'query', { VERBATIM: true }), - ['FT.SEARCH', 'index', 'query', 'VERBATIM'] - ); - }); - - it('with NOSTOPWORDS', () => { - assert.deepEqual( - transformArguments('index', 'query', { NOSTOPWORDS: true }), - ['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS'] - ); - }); - - it('with INKEYS', () => { - assert.deepEqual( - transformArguments('index', 'query', { INKEYS: 'key' }), - ['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key'] - ); - }); - - it('with INFIELDS', () => { - assert.deepEqual( - transformArguments('index', 'query', { INFIELDS: 'field' }), - ['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field'] - ); - }); +describe('FT.SEARCH', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query'), + ['FT.SEARCH', 'index', 'query'] + ); + }); - it('with RETURN', () => { - assert.deepEqual( - transformArguments('index', 'query', { RETURN: 'return' }), - ['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return'] - ); - }); + it('with VERBATIM', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + VERBATIM: true + }), + ['FT.SEARCH', 'index', 'query', 'VERBATIM'] + ); + }); - describe('with SUMMARIZE', () => { - it('true', () => { - assert.deepEqual( - transformArguments('index', 'query', { SUMMARIZE: true }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE'] - ); - }); + it('with NOSTOPWORDS', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + NOSTOPWORDS: true + }), + ['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS'] + ); + }); - describe('with FIELDS', () => { - it('string', () => { - assert.deepEqual( - transformArguments('index', 'query', { - SUMMARIZE: { - FIELDS: ['@field'] - } - }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field'] - ); - }); + it('with INKEYS', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + INKEYS: 'key' + }), + ['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key'] + ); + }); - it('Array', () => { - assert.deepEqual( - transformArguments('index', 'query', { - SUMMARIZE: { - FIELDS: ['@1', '@2'] - } - }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2'] - ); - }); - }); + it('with INFIELDS', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + INFIELDS: 'field' + }), + ['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field'] + ); + }); - it('with FRAGS', () => { - assert.deepEqual( - transformArguments('index', 'query', { - SUMMARIZE: { - FRAGS: 1 - } - }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1'] - ); - }); + it('with RETURN', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + RETURN: 'return' + }), + ['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return'] + ); + }); - it('with LEN', () => { - assert.deepEqual( - transformArguments('index', 'query', { - SUMMARIZE: { - LEN: 1 - } - }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1'] - ); - }); + describe('with SUMMARIZE', () => { + it('true', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + SUMMARIZE: true + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE'] + ); + }); - it('with SEPARATOR', () => { - assert.deepEqual( - transformArguments('index', 'query', { - SUMMARIZE: { - SEPARATOR: 'separator' - } - }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator'] - ); - }); + describe('with FIELDS', () => { + it('string', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + SUMMARIZE: { + FIELDS: '@field' + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field'] + ); }); - describe('with HIGHLIGHT', () => { - it('true', () => { - assert.deepEqual( - transformArguments('index', 'query', { HIGHLIGHT: true }), - ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT'] - ); - }); + it('Array', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + SUMMARIZE: { + FIELDS: ['@1', '@2'] + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2'] + ); + }); + }); - describe('with FIELDS', () => { - it('string', () => { - assert.deepEqual( - transformArguments('index', 'query', { - HIGHLIGHT: { - FIELDS: ['@field'] - } - }), - ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field'] - ); - }); + it('with FRAGS', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + SUMMARIZE: { + FRAGS: 1 + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1'] + ); + }); - it('Array', () => { - assert.deepEqual( - transformArguments('index', 'query', { - HIGHLIGHT: { - FIELDS: ['@1', '@2'] - } - }), - ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2'] - ); - }); - }); + it('with LEN', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + SUMMARIZE: { + LEN: 1 + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1'] + ); + }); - it('with TAGS', () => { - assert.deepEqual( - transformArguments('index', 'query', { - HIGHLIGHT: { - TAGS: { - open: 'open', - close: 'close' - } - } - }), - ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close'] - ); - }); - }); + it('with SEPARATOR', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + SUMMARIZE: { + SEPARATOR: 'separator' + } + }), + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator'] + ); + }); + }); - it('with SLOP', () => { - assert.deepEqual( - transformArguments('index', 'query', { SLOP: 1 }), - ['FT.SEARCH', 'index', 'query', 'SLOP', '1'] - ); - }); + describe('with HIGHLIGHT', () => { + it('true', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + HIGHLIGHT: true + }), + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT'] + ); + }); - it('with INORDER', () => { - assert.deepEqual( - transformArguments('index', 'query', { INORDER: true }), - ['FT.SEARCH', 'index', 'query', 'INORDER'] - ); + describe('with FIELDS', () => { + it('string', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + HIGHLIGHT: { + FIELDS: ['@field'] + } + }), + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field'] + ); }); - it('with LANGUAGE', () => { - assert.deepEqual( - transformArguments('index', 'query', { LANGUAGE: RedisSearchLanguages.ARABIC }), - ['FT.SEARCH', 'index', 'query', 'LANGUAGE', RedisSearchLanguages.ARABIC] - ); + it('Array', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + HIGHLIGHT: { + FIELDS: ['@1', '@2'] + } + }), + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2'] + ); }); + }); - it('with EXPANDER', () => { - assert.deepEqual( - transformArguments('index', 'query', { EXPANDER: 'expender' }), - ['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender'] - ); - }); + it('with TAGS', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + HIGHLIGHT: { + TAGS: { + open: 'open', + close: 'close' + } + } + }), + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close'] + ); + }); + }); - it('with SCORER', () => { - assert.deepEqual( - transformArguments('index', 'query', { SCORER: 'scorer' }), - ['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer'] - ); - }); + it('with SLOP', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + SLOP: 1 + }), + ['FT.SEARCH', 'index', 'query', 'SLOP', '1'] + ); + }); - it('with SORTBY', () => { - assert.deepEqual( - transformArguments('index', 'query', { SORTBY: '@by' }), - ['FT.SEARCH', 'index', 'query', 'SORTBY', '@by'] - ); - }); + it('with TIMEOUT', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + TIMEOUT: 1 + }), + ['FT.SEARCH', 'index', 'query', 'TIMEOUT', '1'] + ); + }); - it('with LIMIT', () => { - assert.deepEqual( - transformArguments('index', 'query', { - LIMIT: { - from: 0, - size: 1 - } - }), - ['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1'] - ); - }); + it('with INORDER', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + INORDER: true + }), + ['FT.SEARCH', 'index', 'query', 'INORDER'] + ); + }); - it('with PARAMS', () => { - assert.deepEqual( - transformArguments('index', 'query', { - PARAMS: { - param: 'value' - } - }), - ['FT.SEARCH', 'index', 'query', 'PARAMS', '2', 'param', 'value'] - ); - }); + it('with LANGUAGE', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + LANGUAGE: 'Arabic' + }), + ['FT.SEARCH', 'index', 'query', 'LANGUAGE', 'Arabic'] + ); + }); - it('with DIALECT', () => { - assert.deepEqual( - transformArguments('index', 'query', { - DIALECT: 1 - }), - ['FT.SEARCH', 'index', 'query', 'DIALECT', '1'] - ); - }); + it('with EXPANDER', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + EXPANDER: 'expender' + }), + ['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender'] + ); + }); - it('with TIMEOUT', () => { - assert.deepEqual( - transformArguments('index', 'query', { - TIMEOUT: 5 - }), - ['FT.SEARCH', 'index', 'query', 'TIMEOUT', '5'] - ); - }); + it('with SCORER', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + SCORER: 'scorer' + }), + ['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer'] + ); }); - describe('client.ft.search', () => { - testUtils.testWithClient('without optional options', async client => { - await Promise.all([ - client.ft.create('index', { - field: SchemaFieldTypes.NUMERIC - }), - client.hSet('1', 'field', '1') - ]); + it('with SORTBY', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + SORTBY: '@by' + }), + ['FT.SEARCH', 'index', 'query', 'SORTBY', '@by'] + ); + }); - assert.deepEqual( - await client.ft.search('index', '*'), - { - total: 1, - documents: [{ - id: '1', - value: Object.create(null, { - field: { - value: '1', - configurable: true, - enumerable: true - } - }) - }] - } - ); - }, GLOBAL.SERVERS.OPEN); + it('with LIMIT', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + LIMIT: { + from: 0, + size: 1 + } + }), + ['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1'] + ); + }); - testUtils.testWithClient('RETURN []', async client => { - await Promise.all([ - client.ft.create('index', { - field: SchemaFieldTypes.NUMERIC - }), - client.hSet('1', 'field', '1'), - client.hSet('2', 'field', '2') - ]); + it('with PARAMS', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + PARAMS: { + string: 'string', + buffer: Buffer.from('buffer'), + number: 1 + } + }), + ['FT.SEARCH', 'index', 'query', 'PARAMS', '6', 'string', 'string', 'buffer', Buffer.from('buffer'), 'number', '1'] + ); + }); - assert.deepEqual( - await client.ft.search('index', '*', { - RETURN: [] - }), - { - total: 2, - documents: [{ - id: '1', - value: Object.create(null) - }, { - id: '2', - value: Object.create(null) - }] - } - ); - }, GLOBAL.SERVERS.OPEN); + it('with DIALECT', () => { + assert.deepEqual( + SEARCH.transformArguments('index', 'query', { + DIALECT: 1 + }), + ['FT.SEARCH', 'index', 'query', 'DIALECT', '1'] + ); }); + }); + + describe('client.ft.search', () => { + testUtils.testWithClient('without optional options', async client => { + await Promise.all([ + client.ft.create('index', { + field: 'TEXT' + }), + client.hSet('1', 'field', '1') + ]); + + assert.deepEqual( + await client.ft.search('index', '*'), + { + total: 1, + documents: [{ + id: '1', + value: Object.create(null, { + field: { + value: '1', + configurable: true, + enumerable: true + } + }) + }] + } + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('RETURN []', async client => { + await Promise.all([ + client.ft.create('index', { + field: 'TEXT' + }), + client.hSet('1', 'field', '1'), + client.hSet('2', 'field', '2') + ]); + + assert.deepEqual( + await client.ft.search('index', '*', { + RETURN: [] + }), + { + total: 2, + documents: [{ + id: '1', + value: Object.create(null) + }, { + id: '2', + value: Object.create(null) + }] + } + ); + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index ff7ab7e201d..1e5e8ec91f5 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -1,109 +1,222 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushSearchOptions, RedisSearchLanguages, Params, PropertyName, SortByProperty, SearchReply } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export interface SearchOptions { - VERBATIM?: true; - NOSTOPWORDS?: true; - // WITHSCORES?: true; - // WITHPAYLOADS?: true; - WITHSORTKEYS?: true; - // FILTER?: { - // field: string; - // min: number | string; - // max: number | string; - // }; - // GEOFILTER?: { - // field: string; - // lon: number; - // lat: number; - // radius: number; - // unit: 'm' | 'km' | 'mi' | 'ft'; - // }; - INKEYS?: string | Array; - INFIELDS?: string | Array; - RETURN?: string | Array; - SUMMARIZE?: true | { - FIELDS?: PropertyName | Array; - FRAGS?: number; - LEN?: number; - SEPARATOR?: string; - }; - HIGHLIGHT?: true | { - FIELDS?: PropertyName | Array; - TAGS?: { - open: string; - close: string; - }; - }; - SLOP?: number; - INORDER?: true; - LANGUAGE?: RedisSearchLanguages; - EXPANDER?: string; - SCORER?: string; - // EXPLAINSCORE?: true; // TODO: WITHSCORES - // PAYLOAD?: ; - SORTBY?: SortByProperty; - // MSORTBY?: SortByProperty | Array; - LIMIT?: { - from: number | string; - size: number | string; +import { RedisArgument, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushOptionalVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { RediSearchProperty, RediSearchLanguage } from './CREATE'; + +export type FtSearchParams = Record; + +export function pushParamsArgument(args: Array, params?: FtSearchParams) { + if (params) { + const length = args.push('PARAMS', ''); + for (const key in params) { + if (!Object.hasOwn(params, key)) continue; + + const value = params[key]; + args.push( + key, + typeof value === 'number' ? value.toString() : value + ); + } + + args[length - 1] = (args.length - length).toString(); + } +} + +export interface FtSearchOptions { + VERBATIM?: boolean; + NOSTOPWORDS?: boolean; + INKEYS?: RedisVariadicArgument; + INFIELDS?: RedisVariadicArgument; + RETURN?: RedisVariadicArgument; + SUMMARIZE?: boolean | { + FIELDS?: RediSearchProperty | Array; + FRAGS?: number; + LEN?: number; + SEPARATOR?: RedisArgument; + }; + HIGHLIGHT?: boolean | { + FIELDS?: RediSearchProperty | Array; + TAGS?: { + open: RedisArgument; + close: RedisArgument; }; - PARAMS?: Params; - DIALECT?: number; - TIMEOUT?: number; + }; + SLOP?: number; + TIMEOUT?: number; + INORDER?: boolean; + LANGUAGE?: RediSearchLanguage; + EXPANDER?: RedisArgument; + SCORER?: RedisArgument; + SORTBY?: RedisArgument | { + BY: RediSearchProperty; + DIRECTION?: 'ASC' | 'DESC'; + }; + LIMIT?: { + from: number | RedisArgument; + size: number | RedisArgument; + }; + PARAMS?: FtSearchParams; + DIALECT?: number; } -export function transformArguments( - index: string, - query: string, - options?: SearchOptions -): RedisCommandArguments { - return pushSearchOptions( - ['FT.SEARCH', index, query], - options - ); +export function pushSearchOptions(args: Array, options?: FtSearchOptions) { + if (options?.VERBATIM) { + args.push('VERBATIM'); + } + + if (options?.NOSTOPWORDS) { + args.push('NOSTOPWORDS'); + } + + pushOptionalVariadicArgument(args, 'INKEYS', options?.INKEYS); + pushOptionalVariadicArgument(args, 'INFIELDS', options?.INFIELDS); + pushOptionalVariadicArgument(args, 'RETURN', options?.RETURN); + + if (options?.SUMMARIZE) { + args.push('SUMMARIZE'); + + if (typeof options.SUMMARIZE === 'object') { + pushOptionalVariadicArgument(args, 'FIELDS', options.SUMMARIZE.FIELDS); + + if (options.SUMMARIZE.FRAGS !== undefined) { + args.push('FRAGS', options.SUMMARIZE.FRAGS.toString()); + } + + if (options.SUMMARIZE.LEN !== undefined) { + args.push('LEN', options.SUMMARIZE.LEN.toString()); + } + + if (options.SUMMARIZE.SEPARATOR !== undefined) { + args.push('SEPARATOR', options.SUMMARIZE.SEPARATOR); + } + } + } + + if (options?.HIGHLIGHT) { + args.push('HIGHLIGHT'); + + if (typeof options.HIGHLIGHT === 'object') { + pushOptionalVariadicArgument(args, 'FIELDS', options.HIGHLIGHT.FIELDS); + + if (options.HIGHLIGHT.TAGS) { + args.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close); + } + } + } + + if (options?.SLOP !== undefined) { + args.push('SLOP', options.SLOP.toString()); + } + + if (options?.TIMEOUT !== undefined) { + args.push('TIMEOUT', options.TIMEOUT.toString()); + } + + if (options?.INORDER) { + args.push('INORDER'); + } + + if (options?.LANGUAGE) { + args.push('LANGUAGE', options.LANGUAGE); + } + + if (options?.EXPANDER) { + args.push('EXPANDER', options.EXPANDER); + } + + if (options?.SCORER) { + args.push('SCORER', options.SCORER); + } + + if (options?.SORTBY) { + args.push('SORTBY'); + + if (typeof options.SORTBY === 'string' || options.SORTBY instanceof Buffer) { + args.push(options.SORTBY); + } else { + args.push(options.SORTBY.BY); + + if (options.SORTBY.DIRECTION) { + args.push(options.SORTBY.DIRECTION); + } + } + } + + if (options?.LIMIT) { + args.push('LIMIT', options.LIMIT.from.toString(), options.LIMIT.size.toString()); + } + + pushParamsArgument(args, options?.PARAMS); + + if (options?.DIALECT !== undefined) { + args.push('DIALECT', options.DIALECT.toString()); + } + + return args; } -export type SearchRawReply = Array; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument, query: RedisArgument, options?: FtSearchOptions) { + const args = ['FT.SEARCH', index, query]; + + return pushSearchOptions(args, options); + }, + transformReply: { + 2: (reply: SearchRawReply): SearchReply => { + const withoutDocuments = (reply[0] + 1 == reply.length) -export function transformReply(reply: SearchRawReply, withoutDocuments: boolean): SearchReply { - const documents = []; - let i = 1; - while (i < reply.length) { + const documents = []; + let i = 1; + while (i < reply.length) { documents.push({ - id: reply[i++], - value: withoutDocuments ? Object.create(null) : documentValue(reply[i++]) + id: reply[i++], + value: withoutDocuments ? Object.create(null) : documentValue(reply[i++]) }); - } - - return { + } + + return { total: reply[0], documents - }; + }; + }, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true +} as const satisfies Command; + +export type SearchRawReply = Array; + +interface SearchDocumentValue { + [key: string]: string | number | null | Array | SearchDocumentValue; +} + +export interface SearchReply { + total: number; + documents: Array<{ + id: string; + value: SearchDocumentValue; + }>; } function documentValue(tuples: any) { - const message = Object.create(null); - - let i = 0; - while (i < tuples.length) { - const key = tuples[i++], - value = tuples[i++]; - if (key === '$') { // might be a JSON reply - try { - Object.assign(message, JSON.parse(value)); - continue; - } catch { - // set as a regular property if not a valid JSON - } - } - - message[key] = value; - } + const message = Object.create(null); + + let i = 0; + while (i < tuples.length) { + const key = tuples[i++], + value = tuples[i++]; + if (key === '$') { // might be a JSON reply + try { + Object.assign(message, JSON.parse(value)); + continue; + } catch { + // set as a regular property if not a valid JSON + } + } + + message[key] = value; + } - return message; + return message; } diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts index da5a6feaba7..be998b9e63d 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts @@ -1,45 +1,34 @@ import { strict as assert } from 'assert'; -import { SchemaFieldTypes } from '.'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments, transformReply } from './SEARCH_NOCONTENT'; +import SEARCH_NOCONTENT from './SEARCH_NOCONTENT'; -describe('SEARCH_NOCONTENT', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('index', 'query'), - ['FT.SEARCH', 'index', 'query', 'NOCONTENT'] - ); - }); +describe('FT.SEARCH NOCONTENT', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + SEARCH_NOCONTENT.transformArguments('index', 'query'), + ['FT.SEARCH', 'index', 'query', 'NOCONTENT'] + ); }); + }); - describe('transformReply', () => { - it('returns total and keys', () => { - assert.deepEqual(transformReply([3, '1', '2', '3']), { - total: 3, - documents: ['1', '2', '3'] - }) - }); - }); - - describe('client.ft.searchNoContent', () => { - testUtils.testWithClient('returns total and keys', async client => { - await Promise.all([ - client.ft.create('index', { - field: SchemaFieldTypes.TEXT - }), - client.hSet('1', 'field', 'field1'), - client.hSet('2', 'field', 'field2'), - client.hSet('3', 'field', 'field3') - ]); + describe('client.ft.searchNoContent', () => { + testUtils.testWithClient('returns total and keys', async client => { + await Promise.all([ + client.ft.create('index', { + field: 'TEXT' + }), + client.hSet('1', 'field', 'field1'), + client.hSet('2', 'field', 'field2') + ]); - assert.deepEqual( - await client.ft.searchNoContent('index', '*'), - { - total: 3, - documents: ['1','2','3'] - } - ); - }, GLOBAL.SERVERS.OPEN); - }); + assert.deepEqual( + await client.ft.searchNoContent('index', '*'), + { + total: 2, + documents: ['1', '2'] + } + ); + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.ts index ab50ae2b9ff..4ee959b9d71 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.ts @@ -1,30 +1,27 @@ -import { RedisCommandArguments } from "@redis/client/dist/lib/commands"; -import { pushSearchOptions } from "."; -import { SearchOptions, SearchRawReply } from "./SEARCH"; +import { Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; +import SEARCH, { SearchRawReply } from './SEARCH'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - index: string, - query: string, - options?: SearchOptions -): RedisCommandArguments { - return pushSearchOptions( - ['FT.SEARCH', index, query, 'NOCONTENT'], - options - ); -} - -export interface SearchNoContentReply { - total: number; - documents: Array; -}; - -export function transformReply(reply: SearchRawReply): SearchNoContentReply { - return { +export default { + FIRST_KEY_INDEX: SEARCH.FIRST_KEY_INDEX, + IS_READ_ONLY: SEARCH.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const redisArgs = SEARCH.transformArguments(...args); + redisArgs.push('NOCONTENT'); + return redisArgs; + }, + transformReply: { + 2: (reply: SearchRawReply): SearchNoContentReply => { + return { total: reply[0], documents: reply.slice(1) - }; -} + } + }, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true +} as const satisfies Command; + +export interface SearchNoContentReply { + total: number; + documents: Array; +}; \ No newline at end of file diff --git a/packages/search/lib/commands/SPELLCHECK.spec.ts b/packages/search/lib/commands/SPELLCHECK.spec.ts index acabbe8a87c..a70ee964920 100644 --- a/packages/search/lib/commands/SPELLCHECK.spec.ts +++ b/packages/search/lib/commands/SPELLCHECK.spec.ts @@ -1,80 +1,79 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { SchemaFieldTypes } from '.'; -import { transformArguments } from './SPELLCHECK'; +import SPELLCHECK from './SPELLCHECK'; -describe('SPELLCHECK', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('index', 'query'), - ['FT.SPELLCHECK', 'index', 'query'] - ); - }); - - it('with DISTANCE', () => { - assert.deepEqual( - transformArguments('index', 'query', { DISTANCE: 2 }), - ['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2'] - ); - }); - - describe('with TERMS', () => { - it('single', () => { - assert.deepEqual( - transformArguments('index', 'query', { - TERMS: { - mode: 'INCLUDE', - dictionary: 'dictionary' - } - }), - ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary'] - ); - }); - - it('multiple', () => { - assert.deepEqual( - transformArguments('index', 'query', { - TERMS: [{ - mode: 'INCLUDE', - dictionary: 'include' - }, { - mode: 'EXCLUDE', - dictionary: 'exclude' - }] - }), - ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude'] - ); - }); - }); +describe('FT.SPELLCHECK', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + SPELLCHECK.transformArguments('index', 'query'), + ['FT.SPELLCHECK', 'index', 'query'] + ); + }); - it('with DIALECT', () => { - assert.deepEqual( - transformArguments('index', 'query', { - DIALECT: 1 - }), - ['FT.SPELLCHECK', 'index', 'query', 'DIALECT', '1'] - ); - }); + it('with DISTANCE', () => { + assert.deepEqual( + SPELLCHECK.transformArguments('index', 'query', { + DISTANCE: 2 + }), + ['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2'] + ); }); - testUtils.testWithClient('client.ft.spellCheck', async client => { - await Promise.all([ - client.ft.create('index', { - field: SchemaFieldTypes.TEXT - }), - client.hSet('key', 'field', 'query') - ]); + describe('with TERMS', () => { + it('single', () => { + assert.deepEqual( + SPELLCHECK.transformArguments('index', 'query', { + TERMS: { + mode: 'INCLUDE', + dictionary: 'dictionary' + } + }), + ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary'] + ); + }); + it('multiple', () => { assert.deepEqual( - await client.ft.spellCheck('index', 'quer'), - [{ - term: 'quer', - suggestions: [{ - score: 1, - suggestion: 'query' - }] + SPELLCHECK.transformArguments('index', 'query', { + TERMS: [{ + mode: 'INCLUDE', + dictionary: 'include' + }, { + mode: 'EXCLUDE', + dictionary: 'exclude' }] + }), + ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude'] ); - }, GLOBAL.SERVERS.OPEN); + }); + }); + + it('with DIALECT', () => { + assert.deepEqual( + SPELLCHECK.transformArguments('index', 'query', { + DIALECT: 1 + }), + ['FT.SPELLCHECK', 'index', 'query', 'DIALECT', '1'] + ); + }); + }); + + testUtils.testWithClient('client.ft.spellCheck', async client => { + const [,, reply] = await Promise.all([ + client.ft.create('index', { + field: 'TEXT' + }), + client.hSet('key', 'field', 'query'), + client.ft.spellCheck('index', 'quer') + ]); + + assert.deepEqual(reply, [{ + term: 'quer', + suggestions: [{ + score: 1, + suggestion: 'query' + }] + }]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/SPELLCHECK.ts b/packages/search/lib/commands/SPELLCHECK.ts index c9317a8b4fe..f52e74ba0f6 100644 --- a/packages/search/lib/commands/SPELLCHECK.ts +++ b/packages/search/lib/commands/SPELLCHECK.ts @@ -1,62 +1,71 @@ -interface SpellCheckTerms { - mode: 'INCLUDE' | 'EXCLUDE'; - dictionary: string; +import { RedisArgument, CommandArguments, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; + +export interface Terms { + mode: 'INCLUDE' | 'EXCLUDE'; + dictionary: RedisArgument; } -interface SpellCheckOptions { - DISTANCE?: number; - TERMS?: SpellCheckTerms | Array; - DIALECT?: number; +export interface FtSpellCheckOptions { + DISTANCE?: number; + TERMS?: Terms | Array; + DIALECT?: number; } -export function transformArguments(index: string, query: string, options?: SpellCheckOptions): Array { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument, query: RedisArgument, options?: FtSpellCheckOptions) { const args = ['FT.SPELLCHECK', index, query]; if (options?.DISTANCE) { - args.push('DISTANCE', options.DISTANCE.toString()); + args.push('DISTANCE', options.DISTANCE.toString()); } if (options?.TERMS) { - if (Array.isArray(options.TERMS)) { - for (const term of options.TERMS) { - pushTerms(args, term); - } - } else { - pushTerms(args, options.TERMS); + if (Array.isArray(options.TERMS)) { + for (const term of options.TERMS) { + pushTerms(args, term); } + } else { + pushTerms(args, options.TERMS); + } } if (options?.DIALECT) { - args.push('DIALECT', options.DIALECT.toString()); + args.push('DIALECT', options.DIALECT.toString()); } return args; -} - -function pushTerms(args: Array, { mode, dictionary }: SpellCheckTerms): void { - args.push('TERMS', mode, dictionary); -} + }, + transformReply: { + 2: (rawReply: SpellCheckRawReply): SpellCheckReply => { + return rawReply.map(([, term, suggestions]) => ({ + term, + suggestions: suggestions.map(([score, suggestion]) => ({ + score: Number(score), + suggestion + })) + })); + }, + 3: undefined as unknown as () => ReplyUnion, + }, + unstableResp3: true +} as const satisfies Command; type SpellCheckRawReply = Array<[ - _: string, - term: string, - suggestions: Array<[score: string, suggestion: string]> + _: string, + term: string, + suggestions: Array<[score: string, suggestion: string]> ]>; type SpellCheckReply = Array<{ - term: string, - suggestions: Array<{ - score: number, - suggestion: string - }> + term: string, + suggestions: Array<{ + score: number, + suggestion: string + }> }>; -export function transformReply(rawReply: SpellCheckRawReply): SpellCheckReply { - return rawReply.map(([, term, suggestions]) => ({ - term, - suggestions: suggestions.map(([score, suggestion]) => ({ - score: Number(score), - suggestion - })) - })); +function pushTerms(args: CommandArguments, { mode, dictionary }: Terms) { + args.push('TERMS', mode, dictionary); } diff --git a/packages/search/lib/commands/SUGADD.spec.ts b/packages/search/lib/commands/SUGADD.spec.ts index 23294eb4abd..24e03d37796 100644 --- a/packages/search/lib/commands/SUGADD.spec.ts +++ b/packages/search/lib/commands/SUGADD.spec.ts @@ -1,35 +1,35 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUGADD'; +import SUGADD from './SUGADD'; -describe('SUGADD', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key', 'string', 1), - ['FT.SUGADD', 'key', 'string', '1'] - ); - }); +describe('FT.SUGADD', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + SUGADD.transformArguments('key', 'string', 1), + ['FT.SUGADD', 'key', 'string', '1'] + ); + }); - it('with INCR', () => { - assert.deepEqual( - transformArguments('key', 'string', 1, { INCR: true }), - ['FT.SUGADD', 'key', 'string', '1', 'INCR'] - ); - }); + it('with INCR', () => { + assert.deepEqual( + SUGADD.transformArguments('key', 'string', 1, { INCR: true }), + ['FT.SUGADD', 'key', 'string', '1', 'INCR'] + ); + }); - it('with PAYLOAD', () => { - assert.deepEqual( - transformArguments('key', 'string', 1, { PAYLOAD: 'payload' }), - ['FT.SUGADD', 'key', 'string', '1', 'PAYLOAD', 'payload'] - ); - }); + it('with PAYLOAD', () => { + assert.deepEqual( + SUGADD.transformArguments('key', 'string', 1, { PAYLOAD: 'payload' }), + ['FT.SUGADD', 'key', 'string', '1', 'PAYLOAD', 'payload'] + ); }); + }); - testUtils.testWithClient('client.ft.sugAdd', async client => { - assert.equal( - await client.ft.sugAdd('key', 'string', 1), - 1 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ft.sugAdd', async client => { + assert.equal( + await client.ft.sugAdd('key', 'string', 1), + 1 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/SUGADD.ts b/packages/search/lib/commands/SUGADD.ts index d68f0d98841..c18cd7846ed 100644 --- a/packages/search/lib/commands/SUGADD.ts +++ b/packages/search/lib/commands/SUGADD.ts @@ -1,20 +1,25 @@ -interface SugAddOptions { - INCR?: true; - PAYLOAD?: string; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; + +export interface FtSugAddOptions { + INCR?: boolean; + PAYLOAD?: RedisArgument; } -export function transformArguments(key: string, string: string, score: number, options?: SugAddOptions): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, string: RedisArgument, score: number, options?: FtSugAddOptions) { const args = ['FT.SUGADD', key, string, score.toString()]; if (options?.INCR) { - args.push('INCR'); + args.push('INCR'); } if (options?.PAYLOAD) { - args.push('PAYLOAD', options.PAYLOAD); + args.push('PAYLOAD', options.PAYLOAD); } return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/search/lib/commands/SUGDEL.spec.ts b/packages/search/lib/commands/SUGDEL.spec.ts index 3d89e3b9a72..ea92c2a1a49 100644 --- a/packages/search/lib/commands/SUGDEL.spec.ts +++ b/packages/search/lib/commands/SUGDEL.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUGDEL'; +import SUGDEL from './SUGDEL'; -describe('SUGDEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'string'), - ['FT.SUGDEL', 'key', 'string'] - ); - }); +describe('FT.SUGDEL', () => { + it('transformArguments', () => { + assert.deepEqual( + SUGDEL.transformArguments('key', 'string'), + ['FT.SUGDEL', 'key', 'string'] + ); + }); - testUtils.testWithClient('client.ft.sugDel', async client => { - assert.equal( - await client.ft.sugDel('key', 'string'), - false - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ft.sugDel', async client => { + assert.equal( + await client.ft.sugDel('key', 'string'), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/SUGDEL.ts b/packages/search/lib/commands/SUGDEL.ts index b522acdfd47..5829ec40a2c 100644 --- a/packages/search/lib/commands/SUGDEL.ts +++ b/packages/search/lib/commands/SUGDEL.ts @@ -1,5 +1,10 @@ -export function transformArguments(key: string, string: string): Array { - return ['FT.SUGDEL', key, string]; -} +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers'; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, string: RedisArgument) { + return ['FT.SUGDEL', key, string]; + }, + transformReply: undefined as unknown as () => NumberReply<0 | 1> +} as const satisfies Command; diff --git a/packages/search/lib/commands/SUGGET.spec.ts b/packages/search/lib/commands/SUGGET.spec.ts index c24c2ff0863..6ea4c03f325 100644 --- a/packages/search/lib/commands/SUGGET.spec.ts +++ b/packages/search/lib/commands/SUGGET.spec.ts @@ -1,46 +1,46 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUGGET'; +import SUGGET from './SUGGET'; -describe('SUGGET', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key', 'prefix'), - ['FT.SUGGET', 'key', 'prefix'] - ); - }); +describe('FT.SUGGET', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + SUGGET.transformArguments('key', 'prefix'), + ['FT.SUGGET', 'key', 'prefix'] + ); + }); - it('with FUZZY', () => { - assert.deepEqual( - transformArguments('key', 'prefix', { FUZZY: true }), - ['FT.SUGGET', 'key', 'prefix', 'FUZZY'] - ); - }); + it('with FUZZY', () => { + assert.deepEqual( + SUGGET.transformArguments('key', 'prefix', { FUZZY: true }), + ['FT.SUGGET', 'key', 'prefix', 'FUZZY'] + ); + }); - it('with MAX', () => { - assert.deepEqual( - transformArguments('key', 'prefix', { MAX: 10 }), - ['FT.SUGGET', 'key', 'prefix', 'MAX', '10'] - ); - }); + it('with MAX', () => { + assert.deepEqual( + SUGGET.transformArguments('key', 'prefix', { MAX: 10 }), + ['FT.SUGGET', 'key', 'prefix', 'MAX', '10'] + ); }); + }); - describe('client.ft.sugGet', () => { - testUtils.testWithClient('null', async client => { - assert.equal( - await client.ft.sugGet('key', 'prefix'), - null - ); - }, GLOBAL.SERVERS.OPEN); + describe('client.ft.sugGet', () => { + testUtils.testWithClient('null', async client => { + assert.equal( + await client.ft.sugGet('key', 'prefix'), + null + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with suggestions', async client => { - await client.ft.sugAdd('key', 'string', 1); + testUtils.testWithClient('with suggestions', async client => { + const [, reply] = await Promise.all([ + client.ft.sugAdd('key', 'string', 1), + client.ft.sugGet('key', 's') + ]); - assert.deepEqual( - await client.ft.sugGet('key', 'string'), - ['string'] - ); - }, GLOBAL.SERVERS.OPEN); - }); + assert.deepEqual(reply, ['string']); + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/search/lib/commands/SUGGET.ts b/packages/search/lib/commands/SUGGET.ts index 558cedeaa08..53dc57a86aa 100644 --- a/packages/search/lib/commands/SUGGET.ts +++ b/packages/search/lib/commands/SUGGET.ts @@ -1,22 +1,25 @@ -export const IS_READ_ONLY = true; +import { NullReply, ArrayReply, BlobStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -export interface SugGetOptions { - FUZZY?: true; - MAX?: number; +export interface FtSugGetOptions { + FUZZY?: boolean; + MAX?: number; } -export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, prefix: RedisArgument, options?: FtSugGetOptions) { const args = ['FT.SUGGET', key, prefix]; if (options?.FUZZY) { - args.push('FUZZY'); + args.push('FUZZY'); } - if (options?.MAX) { - args.push('MAX', options.MAX.toString()); + if (options?.MAX !== undefined) { + args.push('MAX', options.MAX.toString()); } return args; -} - -export declare function transformReply(): null | Array; + }, + transformReply: undefined as unknown as () => NullReply | ArrayReply +} as const satisfies Command; diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts index a4a87ebe895..42a427ce1f4 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts @@ -1,33 +1,35 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUGGET_WITHPAYLOADS'; +import SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS'; -describe('SUGGET WITHPAYLOADS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'prefix'), - ['FT.SUGGET', 'key', 'prefix', 'WITHPAYLOADS'] - ); - }); +describe('FT.SUGGET WITHPAYLOADS', () => { + it('transformArguments', () => { + assert.deepEqual( + SUGGET_WITHPAYLOADS.transformArguments('key', 'prefix'), + ['FT.SUGGET', 'key', 'prefix', 'WITHPAYLOADS'] + ); + }); - describe('client.ft.sugGetWithPayloads', () => { - testUtils.testWithClient('null', async client => { - assert.equal( - await client.ft.sugGetWithPayloads('key', 'prefix'), - null - ); - }, GLOBAL.SERVERS.OPEN); + describe('client.ft.sugGetWithPayloads', () => { + testUtils.testWithClient('null', async client => { + assert.equal( + await client.ft.sugGetWithPayloads('key', 'prefix'), + null + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with suggestions', async client => { - await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' }); + testUtils.testWithClient('with suggestions', async client => { + const [, reply] = await Promise.all([ + client.ft.sugAdd('key', 'string', 1, { + PAYLOAD: 'payload' + }), + client.ft.sugGetWithPayloads('key', 'string') + ]); - assert.deepEqual( - await client.ft.sugGetWithPayloads('key', 'string'), - [{ - suggestion: 'string', - payload: 'payload' - }] - ); - }, GLOBAL.SERVERS.OPEN); - }); + assert.deepEqual(reply, [{ + suggestion: 'string', + payload: 'payload' + }]); + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts index 7eaff4697e1..d8b097f3dbc 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts @@ -1,29 +1,31 @@ -import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET'; +import { NullReply, ArrayReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { isNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import SUGGET from './SUGGET'; -export { IS_READ_ONLY } from './SUGGET'; +export default { + FIRST_KEY_INDEX: SUGGET.FIRST_KEY_INDEX, + IS_READ_ONLY: SUGGET.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const transformedArguments = SUGGET.transformArguments(...args); + transformedArguments.push('WITHPAYLOADS'); + return transformedArguments; + }, + transformReply(reply: NullReply | UnwrapReply>) { + if (isNullReply(reply)) return null; -export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array { - return [ - ...transformSugGetArguments(key, prefix, options), - 'WITHPAYLOADS' - ]; -} - -export interface SuggestionWithPayload { - suggestion: string; - payload: string | null; -} - -export function transformReply(rawReply: Array | null): Array | null { - if (rawReply === null) return null; - - const transformedReply = []; - for (let i = 0; i < rawReply.length; i += 2) { - transformedReply.push({ - suggestion: rawReply[i]!, - payload: rawReply[i + 1] - }); + const transformedReply: Array<{ + suggestion: BlobStringReply; + payload: BlobStringReply; + }> = new Array(reply.length / 2); + let replyIndex = 0, + arrIndex = 0; + while (replyIndex < reply.length) { + transformedReply[arrIndex++] = { + suggestion: reply[replyIndex++], + payload: reply[replyIndex++] + }; } return transformedReply; -} + } +} as const satisfies Command; diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts index e60daa917a9..6969be7729d 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts @@ -1,33 +1,33 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUGGET_WITHSCORES'; +import SUGGET_WITHSCORES from './SUGGET_WITHSCORES'; -describe('SUGGET WITHSCORES', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'prefix'), - ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES'] - ); - }); +describe('FT.SUGGET WITHSCORES', () => { + it('transformArguments', () => { + assert.deepEqual( + SUGGET_WITHSCORES.transformArguments('key', 'prefix'), + ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES'] + ); + }); - describe('client.ft.sugGetWithScores', () => { - testUtils.testWithClient('null', async client => { - assert.equal( - await client.ft.sugGetWithScores('key', 'prefix'), - null - ); - }, GLOBAL.SERVERS.OPEN); + describe('client.ft.sugGetWithScores', () => { + testUtils.testWithClient('null', async client => { + assert.equal( + await client.ft.sugGetWithScores('key', 'prefix'), + null + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with suggestions', async client => { - await client.ft.sugAdd('key', 'string', 1); + testUtils.testWithClient('with suggestions', async client => { + const [, reply] = await Promise.all([ + client.ft.sugAdd('key', 'string', 1), + client.ft.sugGetWithScores('key', 's') + ]); - assert.deepEqual( - await client.ft.sugGetWithScores('key', 'string'), - [{ - suggestion: 'string', - score: 2147483648 - }] - ); - }, GLOBAL.SERVERS.OPEN); - }); + assert.ok(Array.isArray(reply)); + assert.equal(reply.length, 1); + assert.equal(reply[0].suggestion, 'string'); + assert.equal(typeof reply[0].score, 'number'); + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.ts index bad5bff2999..9d24d95cbb0 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.ts @@ -1,29 +1,50 @@ -import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET'; +import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import SUGGET from './SUGGET'; -export { IS_READ_ONLY } from './SUGGET'; - -export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array { - return [ - ...transformSugGetArguments(key, prefix, options), - 'WITHSCORES' - ]; +type SuggestScore = { + suggestion: BlobStringReply; + score: DoubleReply; } -export interface SuggestionWithScores { - suggestion: string; - score: number; -} +export default { + FIRST_KEY_INDEX: SUGGET.FIRST_KEY_INDEX, + IS_READ_ONLY: SUGGET.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const transformedArguments = SUGGET.transformArguments(...args); + transformedArguments.push('WITHSCORES'); + return transformedArguments; + }, + transformReply: { + 2: (reply: NullReply | UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { + if (isNullReply(reply)) return null; -export function transformReply(rawReply: Array | null): Array | null { - if (rawReply === null) return null; + const transformedReply: Array = new Array(reply.length / 2); + let replyIndex = 0, + arrIndex = 0; + while (replyIndex < reply.length) { + transformedReply[arrIndex++] = { + suggestion: reply[replyIndex++], + score: transformDoubleReply[2](reply[replyIndex++], preserve, typeMapping) + }; + } - const transformedReply = []; - for (let i = 0; i < rawReply.length; i += 2) { - transformedReply.push({ - suggestion: rawReply[i], - score: Number(rawReply[i + 1]) - }); - } + return transformedReply; + }, + 3: (reply: UnwrapReply>) => { + if (isNullReply(reply)) return null; + + const transformedReply: Array = new Array(reply.length / 2); + let replyIndex = 0, + arrIndex = 0; + while (replyIndex < reply.length) { + transformedReply[arrIndex++] = { + suggestion: reply[replyIndex++] as BlobStringReply, + score: reply[replyIndex++] as DoubleReply + }; + } - return transformedReply; -} + return transformedReply; + } + } +} as const satisfies Command; diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts index 0900d91b8d9..98aad1c8028 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts @@ -1,34 +1,36 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUGGET_WITHSCORES_WITHPAYLOADS'; +import SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS'; -describe('SUGGET WITHSCORES WITHPAYLOADS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', 'prefix'), - ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES', 'WITHPAYLOADS'] - ); - }); +describe('FT.SUGGET WITHSCORES WITHPAYLOADS', () => { + it('transformArguments', () => { + assert.deepEqual( + SUGGET_WITHSCORES_WITHPAYLOADS.transformArguments('key', 'prefix'), + ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES', 'WITHPAYLOADS'] + ); + }); - describe('client.ft.sugGetWithScoresWithPayloads', () => { - testUtils.testWithClient('null', async client => { - assert.equal( - await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'), - null - ); - }, GLOBAL.SERVERS.OPEN); + describe('client.ft.sugGetWithScoresWithPayloads', () => { + testUtils.testWithClient('null', async client => { + assert.equal( + await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'), + null + ); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with suggestions', async client => { - await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' }); + testUtils.testWithClient('with suggestions', async client => { + const [, reply] = await Promise.all([ + client.ft.sugAdd('key', 'string', 1, { + PAYLOAD: 'payload' + }), + client.ft.sugGetWithScoresWithPayloads('key', 'string') + ]); - assert.deepEqual( - await client.ft.sugGetWithScoresWithPayloads('key', 'string'), - [{ - suggestion: 'string', - score: 2147483648, - payload: 'payload' - }] - ); - }, GLOBAL.SERVERS.OPEN); - }); + assert.ok(Array.isArray(reply)); + assert.equal(reply.length, 1); + assert.equal(reply[0].suggestion, 'string'); + assert.equal(typeof reply[0].score, 'number'); + assert.equal(reply[0].payload, 'payload'); + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts index 3b2fe7667b7..1e125eb15fa 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts @@ -1,30 +1,56 @@ -import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET'; -import { SuggestionWithPayload } from './SUGGET_WITHPAYLOADS'; -import { SuggestionWithScores } from './SUGGET_WITHSCORES'; +import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import SUGGET from './SUGGET'; -export { IS_READ_ONLY } from './SUGGET'; - -export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array { - return [ - ...transformSugGetArguments(key, prefix, options), - 'WITHSCORES', - 'WITHPAYLOADS' - ]; +type SuggestScoreWithPayload = { + suggestion: BlobStringReply; + score: DoubleReply; + payload: BlobStringReply; } -type SuggestionWithScoresAndPayloads = SuggestionWithScores & SuggestionWithPayload; +export default { + FIRST_KEY_INDEX: SUGGET.FIRST_KEY_INDEX, + IS_READ_ONLY: SUGGET.IS_READ_ONLY, + transformArguments(...args: Parameters) { + const transformedArguments = SUGGET.transformArguments(...args); + transformedArguments.push( + 'WITHSCORES', + 'WITHPAYLOADS' + ); + return transformedArguments; + }, + transformReply: { + 2: (reply: NullReply | UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { + if (isNullReply(reply)) return null; -export function transformReply(rawReply: Array | null): Array | null { - if (rawReply === null) return null; + const transformedReply: Array = new Array(reply.length / 3); + let replyIndex = 0, + arrIndex = 0; + while (replyIndex < reply.length) { + transformedReply[arrIndex++] = { + suggestion: reply[replyIndex++], + score: transformDoubleReply[2](reply[replyIndex++], preserve, typeMapping), + payload: reply[replyIndex++] + }; + } - const transformedReply = []; - for (let i = 0; i < rawReply.length; i += 3) { - transformedReply.push({ - suggestion: rawReply[i]!, - score: Number(rawReply[i + 1]!), - payload: rawReply[i + 2] - }); - } + return transformedReply; + }, + 3: (reply: NullReply | UnwrapReply>) => { + if (isNullReply(reply)) return null; - return transformedReply; -} + const transformedReply: Array = new Array(reply.length / 3); + let replyIndex = 0, + arrIndex = 0; + while (replyIndex < reply.length) { + transformedReply[arrIndex++] = { + suggestion: reply[replyIndex++] as BlobStringReply, + score: reply[replyIndex++] as DoubleReply, + payload: reply[replyIndex++] as BlobStringReply + }; + } + + return transformedReply; + } + } +} as const satisfies Command; diff --git a/packages/search/lib/commands/SUGLEN.spec.ts b/packages/search/lib/commands/SUGLEN.spec.ts index 2ea680df953..6e6d5e1fc5c 100644 --- a/packages/search/lib/commands/SUGLEN.spec.ts +++ b/packages/search/lib/commands/SUGLEN.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SUGLEN'; +import SUGLEN from './SUGLEN'; -describe('SUGLEN', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key'), - ['FT.SUGLEN', 'key'] - ); - }); +describe('FT.SUGLEN', () => { + it('transformArguments', () => { + assert.deepEqual( + SUGLEN.transformArguments('key'), + ['FT.SUGLEN', 'key'] + ); + }); - testUtils.testWithClient('client.ft.sugLen', async client => { - assert.equal( - await client.ft.sugLen('key'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ft.sugLen', async client => { + assert.equal( + await client.ft.sugLen('key'), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/SUGLEN.ts b/packages/search/lib/commands/SUGLEN.ts index 15b3da61261..85dde8cfb70 100644 --- a/packages/search/lib/commands/SUGLEN.ts +++ b/packages/search/lib/commands/SUGLEN.ts @@ -1,7 +1,10 @@ -export const IS_READ_ONLY = true; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(key: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument) { return ['FT.SUGLEN', key]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/search/lib/commands/SYNDUMP.spec.ts b/packages/search/lib/commands/SYNDUMP.spec.ts index 472db54bcf8..59c010a8d6d 100644 --- a/packages/search/lib/commands/SYNDUMP.spec.ts +++ b/packages/search/lib/commands/SYNDUMP.spec.ts @@ -1,24 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SYNDUMP'; -import { SchemaFieldTypes } from '.'; +import SYNDUMP from './SYNDUMP'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; -describe('SYNDUMP', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('index'), - ['FT.SYNDUMP', 'index'] - ); - }); +describe('FT.SYNDUMP', () => { + it('transformArguments', () => { + assert.deepEqual( + SYNDUMP.transformArguments('index'), + ['FT.SYNDUMP', 'index'] + ); + }); - testUtils.testWithClient('client.ft.synDump', async client => { - await client.ft.create('index', { - field: SchemaFieldTypes.TEXT - }); + testUtils.testWithClient('client.ft.synDump', async client => { + const [, reply] = await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }), + client.ft.synDump('index') + ]); - assert.deepEqual( - await client.ft.synDump('index'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, {}); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/SYNDUMP.ts b/packages/search/lib/commands/SYNDUMP.ts index 5f1e71aaf78..2fe7540fda5 100644 --- a/packages/search/lib/commands/SYNDUMP.ts +++ b/packages/search/lib/commands/SYNDUMP.ts @@ -1,5 +1,22 @@ -export function transformArguments(index: string): Array { - return ['FT.SYNDUMP', index]; -} +import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument) { + return ['FT.SYNDUMP', index]; + }, + transformReply: { + 2: (reply: UnwrapReply>>) => { + const result: Record> = {}; + let i = 0; + while (i < reply.length) { + const key = (reply[i++] as unknown as UnwrapReply).toString(), + value = reply[i++] as unknown as ArrayReply; + result[key] = value; + } + return result; + }, + 3: undefined as unknown as () => MapReply> + } +} as const satisfies Command; diff --git a/packages/search/lib/commands/SYNUPDATE.spec.ts b/packages/search/lib/commands/SYNUPDATE.spec.ts index 19ac9b85e54..e901ae9fe3f 100644 --- a/packages/search/lib/commands/SYNUPDATE.spec.ts +++ b/packages/search/lib/commands/SYNUPDATE.spec.ts @@ -1,40 +1,42 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './SYNUPDATE'; -import { SchemaFieldTypes } from '.'; +import SYNUPDATE from './SYNUPDATE'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; -describe('SYNUPDATE', () => { - describe('transformArguments', () => { - it('single term', () => { - assert.deepEqual( - transformArguments('index', 'groupId', 'term'), - ['FT.SYNUPDATE', 'index', 'groupId', 'term'] - ); - }); +describe('FT.SYNUPDATE', () => { + describe('transformArguments', () => { + it('single term', () => { + assert.deepEqual( + SYNUPDATE.transformArguments('index', 'groupId', 'term'), + ['FT.SYNUPDATE', 'index', 'groupId', 'term'] + ); + }); - it('multiple terms', () => { - assert.deepEqual( - transformArguments('index', 'groupId', ['1', '2']), - ['FT.SYNUPDATE', 'index', 'groupId', '1', '2'] - ); - }); + it('multiple terms', () => { + assert.deepEqual( + SYNUPDATE.transformArguments('index', 'groupId', ['1', '2']), + ['FT.SYNUPDATE', 'index', 'groupId', '1', '2'] + ); + }); - it('with SKIPINITIALSCAN', () => { - assert.deepEqual( - transformArguments('index', 'groupId', 'term', { SKIPINITIALSCAN: true }), - ['FT.SYNUPDATE', 'index', 'groupId', 'SKIPINITIALSCAN', 'term'] - ); - }); + it('with SKIPINITIALSCAN', () => { + assert.deepEqual( + SYNUPDATE.transformArguments('index', 'groupId', 'term', { + SKIPINITIALSCAN: true + }), + ['FT.SYNUPDATE', 'index', 'groupId', 'SKIPINITIALSCAN', 'term'] + ); }); + }); - testUtils.testWithClient('client.ft.synUpdate', async client => { - await client.ft.create('index', { - field: SchemaFieldTypes.TEXT - }); + testUtils.testWithClient('client.ft.synUpdate', async client => { + const [, reply] = await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }), + client.ft.synUpdate('index', 'groupId', 'term') + ]); - assert.equal( - await client.ft.synUpdate('index', 'groupId', 'term'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/SYNUPDATE.ts b/packages/search/lib/commands/SYNUPDATE.ts index 3384ea59d94..926d8e58e1c 100644 --- a/packages/search/lib/commands/SYNUPDATE.ts +++ b/packages/search/lib/commands/SYNUPDATE.ts @@ -1,23 +1,26 @@ -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -interface SynUpdateOptions { - SKIPINITIALSCAN?: true; +export interface FtSynUpdateOptions { + SKIPINITIALSCAN?: boolean; } -export function transformArguments( - index: string, - groupId: string, - terms: string | Array, - options?: SynUpdateOptions -): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments( + index: RedisArgument, + groupId: RedisArgument, + terms: RedisVariadicArgument, + options?: FtSynUpdateOptions + ) { const args = ['FT.SYNUPDATE', index, groupId]; if (options?.SKIPINITIALSCAN) { - args.push('SKIPINITIALSCAN'); + args.push('SKIPINITIALSCAN'); } - return pushVerdictArguments(args, terms); -} - -export declare function transformReply(): 'OK'; + return pushVariadicArguments(args, terms); + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/search/lib/commands/TAGVALS.spec.ts b/packages/search/lib/commands/TAGVALS.spec.ts index d59bfcfe3ea..dbc6203f93e 100644 --- a/packages/search/lib/commands/TAGVALS.spec.ts +++ b/packages/search/lib/commands/TAGVALS.spec.ts @@ -1,24 +1,24 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { SchemaFieldTypes } from '.'; -import { transformArguments } from './TAGVALS'; +import TAGVALS from './TAGVALS'; +import { SCHEMA_FIELD_TYPE } from './CREATE'; -describe('TAGVALS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('index', '@field'), - ['FT.TAGVALS', 'index', '@field'] - ); - }); +describe('FT.TAGVALS', () => { + it('transformArguments', () => { + assert.deepEqual( + TAGVALS.transformArguments('index', '@field'), + ['FT.TAGVALS', 'index', '@field'] + ); + }); - testUtils.testWithClient('client.ft.tagVals', async client => { - await client.ft.create('index', { - field: SchemaFieldTypes.TAG - }); + testUtils.testWithClient('client.ft.tagVals', async client => { + const [, reply] = await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TAG + }), + client.ft.tagVals('index', 'field') + ]); - assert.deepEqual( - await client.ft.tagVals('index', 'field'), - [] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, []); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/TAGVALS.ts b/packages/search/lib/commands/TAGVALS.ts index 54342f0c9e5..8a6e73c97b8 100644 --- a/packages/search/lib/commands/TAGVALS.ts +++ b/packages/search/lib/commands/TAGVALS.ts @@ -1,5 +1,13 @@ -export function transformArguments(index: string, fieldName: string): Array { - return ['FT.TAGVALS', index, fieldName]; -} +import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(index: RedisArgument, fieldName: RedisArgument) { + return ['FT.TAGVALS', index, fieldName]; + }, + transformReply: { + 2: undefined as unknown as () => ArrayReply, + 3: undefined as unknown as () => SetReply + } +} as const satisfies Command; diff --git a/packages/search/lib/commands/_LIST.spec.ts b/packages/search/lib/commands/_LIST.spec.ts index 602c29975f2..a7f13b011ac 100644 --- a/packages/search/lib/commands/_LIST.spec.ts +++ b/packages/search/lib/commands/_LIST.spec.ts @@ -1,19 +1,19 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './_LIST'; +import _LIST from './_LIST'; describe('_LIST', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments(), - ['FT._LIST'] - ); - }); + it('transformArguments', () => { + assert.deepEqual( + _LIST.transformArguments(), + ['FT._LIST'] + ); + }); - testUtils.testWithClient('client.ft._list', async client => { - assert.deepEqual( - await client.ft._list(), - [] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ft._list', async client => { + assert.deepEqual( + await client.ft._list(), + [] + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/_LIST.ts b/packages/search/lib/commands/_LIST.ts index 588ec837c3b..efb6c31acce 100644 --- a/packages/search/lib/commands/_LIST.ts +++ b/packages/search/lib/commands/_LIST.ts @@ -1,5 +1,13 @@ -export function transformArguments(): Array { - return ['FT._LIST']; -} +import { ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments() { + return ['FT._LIST']; + }, + transformReply: { + 2: undefined as unknown as () => ArrayReply, + 3: undefined as unknown as () => SetReply + } +} as const satisfies Command; diff --git a/packages/search/lib/commands/index.spec.ts b/packages/search/lib/commands/index.spec.ts index 4c54a0dfdf2..04808932c59 100644 --- a/packages/search/lib/commands/index.spec.ts +++ b/packages/search/lib/commands/index.spec.ts @@ -1,5 +1,6 @@ -import { strict as assert } from 'assert'; -import { pushArgumentsWithLength, pushSortByArguments } from '.'; +import { strict as assert } from 'node:assert'; + +/* import { pushArgumentsWithLength, pushSortByArguments } from '.'; describe('pushSortByArguments', () => { describe('single', () => { @@ -44,3 +45,4 @@ it('pushArgumentsWithLength', () => { ['a', '2', 'b', 'c'] ); }); +*/ \ No newline at end of file diff --git a/packages/search/lib/commands/index.ts b/packages/search/lib/commands/index.ts index f907e1999e6..00706a70c2e 100644 --- a/packages/search/lib/commands/index.ts +++ b/packages/search/lib/commands/index.ts @@ -1,690 +1,105 @@ -import * as _LIST from './_LIST'; -import * as ALTER from './ALTER'; -import * as AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR'; -import * as AGGREGATE from './AGGREGATE'; -import * as ALIASADD from './ALIASADD'; -import * as ALIASDEL from './ALIASDEL'; -import * as ALIASUPDATE from './ALIASUPDATE'; -import * as CONFIG_GET from './CONFIG_GET'; -import * as CONFIG_SET from './CONFIG_SET'; -import * as CREATE from './CREATE'; -import * as CURSOR_DEL from './CURSOR_DEL'; -import * as CURSOR_READ from './CURSOR_READ'; -import * as DICTADD from './DICTADD'; -import * as DICTDEL from './DICTDEL'; -import * as DICTDUMP from './DICTDUMP'; -import * as DROPINDEX from './DROPINDEX'; -import * as EXPLAIN from './EXPLAIN'; -import * as EXPLAINCLI from './EXPLAINCLI'; -import * as INFO from './INFO'; -import * as PROFILESEARCH from './PROFILE_SEARCH'; -import * as PROFILEAGGREGATE from './PROFILE_AGGREGATE'; -import * as SEARCH from './SEARCH'; -import * as SEARCH_NOCONTENT from './SEARCH_NOCONTENT'; -import * as SPELLCHECK from './SPELLCHECK'; -import * as SUGADD from './SUGADD'; -import * as SUGDEL from './SUGDEL'; -import * as SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS'; -import * as SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS'; -import * as SUGGET_WITHSCORES from './SUGGET_WITHSCORES'; -import * as SUGGET from './SUGGET'; -import * as SUGLEN from './SUGLEN'; -import * as SYNDUMP from './SYNDUMP'; -import * as SYNUPDATE from './SYNUPDATE'; -import * as TAGVALS from './TAGVALS'; -import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushOptionalVerdictArgument, pushVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { SearchOptions } from './SEARCH'; +import _LIST from './_LIST'; +import ALTER from './ALTER'; +import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR'; +import AGGREGATE from './AGGREGATE'; +import ALIASADD from './ALIASADD'; +import ALIASDEL from './ALIASDEL'; +import ALIASUPDATE from './ALIASUPDATE'; +import CONFIG_GET from './CONFIG_GET'; +import CONFIG_SET from './CONFIG_SET'; +import CREATE from './CREATE'; +import CURSOR_DEL from './CURSOR_DEL'; +import CURSOR_READ from './CURSOR_READ'; +import DICTADD from './DICTADD'; +import DICTDEL from './DICTDEL'; +import DICTDUMP from './DICTDUMP'; +import DROPINDEX from './DROPINDEX'; +import EXPLAIN from './EXPLAIN'; +import EXPLAINCLI from './EXPLAINCLI'; +import INFO from './INFO'; +import PROFILESEARCH from './PROFILE_SEARCH'; +import PROFILEAGGREGATE from './PROFILE_AGGREGATE'; +import SEARCH_NOCONTENT from './SEARCH_NOCONTENT'; +import SEARCH from './SEARCH'; +import SPELLCHECK from './SPELLCHECK'; +import SUGADD from './SUGADD'; +import SUGDEL from './SUGDEL'; +import SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS'; +import SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS'; +import SUGGET_WITHSCORES from './SUGGET_WITHSCORES'; +import SUGGET from './SUGGET'; +import SUGLEN from './SUGLEN'; +import SYNDUMP from './SYNDUMP'; +import SYNUPDATE from './SYNUPDATE'; +import TAGVALS from './TAGVALS'; export default { - _LIST, - _list: _LIST, - ALTER, - alter: ALTER, - AGGREGATE_WITHCURSOR, - aggregateWithCursor: AGGREGATE_WITHCURSOR, - AGGREGATE, - aggregate: AGGREGATE, - ALIASADD, - aliasAdd: ALIASADD, - ALIASDEL, - aliasDel: ALIASDEL, - ALIASUPDATE, - aliasUpdate: ALIASUPDATE, - CONFIG_GET, - configGet: CONFIG_GET, - CONFIG_SET, - configSet: CONFIG_SET, - CREATE, - create: CREATE, - CURSOR_DEL, - cursorDel: CURSOR_DEL, - CURSOR_READ, - cursorRead: CURSOR_READ, - DICTADD, - dictAdd: DICTADD, - DICTDEL, - dictDel: DICTDEL, - DICTDUMP, - dictDump: DICTDUMP, - DROPINDEX, - dropIndex: DROPINDEX, - EXPLAIN, - explain: EXPLAIN, - EXPLAINCLI, - explainCli: EXPLAINCLI, - INFO, - info: INFO, - PROFILESEARCH, - profileSearch: PROFILESEARCH, - PROFILEAGGREGATE, - profileAggregate: PROFILEAGGREGATE, - SEARCH, - search: SEARCH, - SEARCH_NOCONTENT, - searchNoContent: SEARCH_NOCONTENT, - SPELLCHECK, - spellCheck: SPELLCHECK, - SUGADD, - sugAdd: SUGADD, - SUGDEL, - sugDel: SUGDEL, - SUGGET_WITHPAYLOADS, - sugGetWithPayloads: SUGGET_WITHPAYLOADS, - SUGGET_WITHSCORES_WITHPAYLOADS, - sugGetWithScoresWithPayloads: SUGGET_WITHSCORES_WITHPAYLOADS, - SUGGET_WITHSCORES, - sugGetWithScores: SUGGET_WITHSCORES, - SUGGET, - sugGet: SUGGET, - SUGLEN, - sugLen: SUGLEN, - SYNDUMP, - synDump: SYNDUMP, - SYNUPDATE, - synUpdate: SYNUPDATE, - TAGVALS, - tagVals: TAGVALS + _LIST, + _list: _LIST, + ALTER, + alter: ALTER, + AGGREGATE_WITHCURSOR, + aggregateWithCursor: AGGREGATE_WITHCURSOR, + AGGREGATE, + aggregate: AGGREGATE, + ALIASADD, + aliasAdd: ALIASADD, + ALIASDEL, + aliasDel: ALIASDEL, + ALIASUPDATE, + aliasUpdate: ALIASUPDATE, + CONFIG_GET, + configGet: CONFIG_GET, + CONFIG_SET, + configSet: CONFIG_SET, + CREATE, + create: CREATE, + CURSOR_DEL, + cursorDel: CURSOR_DEL, + CURSOR_READ, + cursorRead: CURSOR_READ, + DICTADD, + dictAdd: DICTADD, + DICTDEL, + dictDel: DICTDEL, + DICTDUMP, + dictDump: DICTDUMP, + DROPINDEX, + dropIndex: DROPINDEX, + EXPLAIN, + explain: EXPLAIN, + EXPLAINCLI, + explainCli: EXPLAINCLI, + INFO, + info: INFO, + PROFILESEARCH, + profileSearch: PROFILESEARCH, + PROFILEAGGREGATE, + profileAggregate: PROFILEAGGREGATE, + SEARCH_NOCONTENT, + searchNoContent: SEARCH_NOCONTENT, + SEARCH, + search: SEARCH, + SPELLCHECK, + spellCheck: SPELLCHECK, + SUGADD, + sugAdd: SUGADD, + SUGDEL, + sugDel: SUGDEL, + SUGGET_WITHPAYLOADS, + sugGetWithPayloads: SUGGET_WITHPAYLOADS, + SUGGET_WITHSCORES_WITHPAYLOADS, + sugGetWithScoresWithPayloads: SUGGET_WITHSCORES_WITHPAYLOADS, + SUGGET_WITHSCORES, + sugGetWithScores: SUGGET_WITHSCORES, + SUGGET, + sugGet: SUGGET, + SUGLEN, + sugLen: SUGLEN, + SYNDUMP, + synDump: SYNDUMP, + SYNUPDATE, + synUpdate: SYNUPDATE, + TAGVALS, + tagVals: TAGVALS }; - -export enum RedisSearchLanguages { - ARABIC = 'Arabic', - BASQUE = 'Basque', - CATALANA = 'Catalan', - DANISH = 'Danish', - DUTCH = 'Dutch', - ENGLISH = 'English', - FINNISH = 'Finnish', - FRENCH = 'French', - GERMAN = 'German', - GREEK = 'Greek', - HUNGARIAN = 'Hungarian', - INDONESAIN = 'Indonesian', - IRISH = 'Irish', - ITALIAN = 'Italian', - LITHUANIAN = 'Lithuanian', - NEPALI = 'Nepali', - NORWEIGAN = 'Norwegian', - PORTUGUESE = 'Portuguese', - ROMANIAN = 'Romanian', - RUSSIAN = 'Russian', - SPANISH = 'Spanish', - SWEDISH = 'Swedish', - TAMIL = 'Tamil', - TURKISH = 'Turkish', - CHINESE = 'Chinese' -} - -export type PropertyName = `${'@' | '$.'}${string}`; - -export type SortByProperty = string | { - BY: string; - DIRECTION?: 'ASC' | 'DESC'; -}; - -export function pushSortByProperty(args: RedisCommandArguments, sortBy: SortByProperty): void { - if (typeof sortBy === 'string') { - args.push(sortBy); - } else { - args.push(sortBy.BY); - - if (sortBy.DIRECTION) { - args.push(sortBy.DIRECTION); - } - } -} - -export function pushSortByArguments(args: RedisCommandArguments, name: string, sortBy: SortByProperty | Array): RedisCommandArguments { - const lengthBefore = args.push( - name, - '' // will be overwritten - ); - - if (Array.isArray(sortBy)) { - for (const field of sortBy) { - pushSortByProperty(args, field); - } - } else { - pushSortByProperty(args, sortBy); - } - - args[lengthBefore - 1] = (args.length - lengthBefore).toString(); - - return args; -} - -export function pushArgumentsWithLength(args: RedisCommandArguments, fn: (args: RedisCommandArguments) => void): RedisCommandArguments { - const lengthIndex = args.push('') - 1; - fn(args); - args[lengthIndex] = (args.length - lengthIndex - 1).toString(); - return args; -} - -export enum SchemaFieldTypes { - TEXT = 'TEXT', - NUMERIC = 'NUMERIC', - GEO = 'GEO', - TAG = 'TAG', - VECTOR = 'VECTOR', - GEOSHAPE = 'GEOSHAPE' -} - -type CreateSchemaField< - T extends SchemaFieldTypes, - E = Record -> = T | ({ - type: T; - AS?: string; - INDEXMISSING?: boolean; -} & E); - -type CommonFieldArguments = { - SORTABLE?: boolean | 'UNF'; - NOINDEX?: boolean; -}; - -type CreateSchemaCommonField< - T extends SchemaFieldTypes, - E = Record -> = CreateSchemaField< - T, - (CommonFieldArguments & E) ->; - -function pushCommonFieldArguments(args: RedisCommandArguments, fieldOptions: CommonFieldArguments) { - if (fieldOptions.SORTABLE) { - args.push('SORTABLE'); - - if (fieldOptions.SORTABLE === 'UNF') { - args.push('UNF'); - } - } - - if (fieldOptions.NOINDEX) { - args.push('NOINDEX'); - } -} - -export enum SchemaTextFieldPhonetics { - DM_EN = 'dm:en', - DM_FR = 'dm:fr', - FM_PT = 'dm:pt', - DM_ES = 'dm:es' -} - -type CreateSchemaTextField = CreateSchemaCommonField; - -type CreateSchemaNumericField = CreateSchemaCommonField; - -type CreateSchemaGeoField = CreateSchemaCommonField; - -type CreateSchemaTagField = CreateSchemaCommonField; - -export enum VectorAlgorithms { - FLAT = 'FLAT', - HNSW = 'HNSW' -} - -type CreateSchemaVectorField< - T extends VectorAlgorithms, - A extends Record -> = CreateSchemaField; - -type CreateSchemaFlatVectorField = CreateSchemaVectorField; - -type CreateSchemaHNSWVectorField = CreateSchemaVectorField; - -export const SCHEMA_GEO_SHAPE_COORD_SYSTEM = { - SPHERICAL: 'SPHERICAL', - FLAT: 'FLAT' -} as const; - -export type SchemaGeoShapeFieldCoordSystem = typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM[keyof typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM]; - -type CreateSchemaGeoShapeField = CreateSchemaCommonField; - -export interface RediSearchSchema { - [field: string]: - CreateSchemaTextField | - CreateSchemaNumericField | - CreateSchemaGeoField | - CreateSchemaTagField | - CreateSchemaFlatVectorField | - CreateSchemaHNSWVectorField | - CreateSchemaGeoShapeField; -} - -export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema) { - for (const [field, fieldOptions] of Object.entries(schema)) { - args.push(field); - - if (typeof fieldOptions === 'string') { - args.push(fieldOptions); - continue; - } - - if (fieldOptions.AS) { - args.push('AS', fieldOptions.AS); - } - - args.push(fieldOptions.type); - - switch (fieldOptions.type) { - case SchemaFieldTypes.TEXT: - if (fieldOptions.NOSTEM) { - args.push('NOSTEM'); - } - - if (fieldOptions.WEIGHT) { - args.push('WEIGHT', fieldOptions.WEIGHT.toString()); - } - - if (fieldOptions.PHONETIC) { - args.push('PHONETIC', fieldOptions.PHONETIC); - } - - if (fieldOptions.WITHSUFFIXTRIE) { - args.push('WITHSUFFIXTRIE'); - } - - pushCommonFieldArguments(args, fieldOptions); - - if (fieldOptions.INDEXEMPTY) { - args.push('INDEXEMPTY'); - } - - break; - - case SchemaFieldTypes.NUMERIC: - case SchemaFieldTypes.GEO: - pushCommonFieldArguments(args, fieldOptions); - break; - - case SchemaFieldTypes.TAG: - if (fieldOptions.SEPARATOR) { - args.push('SEPARATOR', fieldOptions.SEPARATOR); - } - - if (fieldOptions.CASESENSITIVE) { - args.push('CASESENSITIVE'); - } - - if (fieldOptions.WITHSUFFIXTRIE) { - args.push('WITHSUFFIXTRIE'); - } - - pushCommonFieldArguments(args, fieldOptions); - - if (fieldOptions.INDEXEMPTY) { - args.push('INDEXEMPTY'); - } - - break; - - case SchemaFieldTypes.VECTOR: - args.push(fieldOptions.ALGORITHM); - - pushArgumentsWithLength(args, () => { - args.push( - 'TYPE', fieldOptions.TYPE, - 'DIM', fieldOptions.DIM.toString(), - 'DISTANCE_METRIC', fieldOptions.DISTANCE_METRIC - ); - - if (fieldOptions.INITIAL_CAP) { - args.push('INITIAL_CAP', fieldOptions.INITIAL_CAP.toString()); - } - - switch (fieldOptions.ALGORITHM) { - case VectorAlgorithms.FLAT: - if (fieldOptions.BLOCK_SIZE) { - args.push('BLOCK_SIZE', fieldOptions.BLOCK_SIZE.toString()); - } - - break; - - case VectorAlgorithms.HNSW: - if (fieldOptions.M) { - args.push('M', fieldOptions.M.toString()); - } - - if (fieldOptions.EF_CONSTRUCTION) { - args.push('EF_CONSTRUCTION', fieldOptions.EF_CONSTRUCTION.toString()); - } - - if (fieldOptions.EF_RUNTIME) { - args.push('EF_RUNTIME', fieldOptions.EF_RUNTIME.toString()); - } - - break; - } - }); - - break; - - case SchemaFieldTypes.GEOSHAPE: - if (fieldOptions.COORD_SYSTEM !== undefined) { - args.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM); - } - - pushCommonFieldArguments(args, fieldOptions); - - break; - } - - if (fieldOptions.INDEXMISSING) { - args.push('INDEXMISSING'); - } - } -} - -export type Params = Record; - -export function pushParamsArgs( - args: RedisCommandArguments, - params?: Params -): RedisCommandArguments { - if (params) { - const enrties = Object.entries(params); - args.push('PARAMS', (enrties.length * 2).toString()); - for (const [key, value] of enrties) { - args.push(key, typeof value === 'number' ? value.toString() : value); - } - } - - return args; -} - -export function pushSearchOptions( - args: RedisCommandArguments, - options?: SearchOptions -): RedisCommandArguments { - if (options?.VERBATIM) { - args.push('VERBATIM'); - } - - if (options?.NOSTOPWORDS) { - args.push('NOSTOPWORDS'); - } - - // if (options?.WITHSCORES) { - // args.push('WITHSCORES'); - // } - - // if (options?.WITHPAYLOADS) { - // args.push('WITHPAYLOADS'); - // } - - pushOptionalVerdictArgument(args, 'INKEYS', options?.INKEYS); - pushOptionalVerdictArgument(args, 'INFIELDS', options?.INFIELDS); - pushOptionalVerdictArgument(args, 'RETURN', options?.RETURN); - - if (options?.SUMMARIZE) { - args.push('SUMMARIZE'); - - if (typeof options.SUMMARIZE === 'object') { - if (options.SUMMARIZE.FIELDS) { - args.push('FIELDS'); - pushVerdictArgument(args, options.SUMMARIZE.FIELDS); - } - - if (options.SUMMARIZE.FRAGS) { - args.push('FRAGS', options.SUMMARIZE.FRAGS.toString()); - } - - if (options.SUMMARIZE.LEN) { - args.push('LEN', options.SUMMARIZE.LEN.toString()); - } - - if (options.SUMMARIZE.SEPARATOR) { - args.push('SEPARATOR', options.SUMMARIZE.SEPARATOR); - } - } - } - - if (options?.HIGHLIGHT) { - args.push('HIGHLIGHT'); - - if (typeof options.HIGHLIGHT === 'object') { - if (options.HIGHLIGHT.FIELDS) { - args.push('FIELDS'); - pushVerdictArgument(args, options.HIGHLIGHT.FIELDS); - } - - if (options.HIGHLIGHT.TAGS) { - args.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close); - } - } - } - - if (options?.SLOP) { - args.push('SLOP', options.SLOP.toString()); - } - - if (options?.INORDER) { - args.push('INORDER'); - } - - if (options?.LANGUAGE) { - args.push('LANGUAGE', options.LANGUAGE); - } - - if (options?.EXPANDER) { - args.push('EXPANDER', options.EXPANDER); - } - - if (options?.SCORER) { - args.push('SCORER', options.SCORER); - } - - // if (options?.EXPLAINSCORE) { - // args.push('EXPLAINSCORE'); - // } - - // if (options?.PAYLOAD) { - // args.push('PAYLOAD', options.PAYLOAD); - // } - - if (options?.SORTBY) { - args.push('SORTBY'); - pushSortByProperty(args, options.SORTBY); - } - - // if (options?.MSORTBY) { - // pushSortByArguments(args, 'MSORTBY', options.MSORTBY); - // } - - if (options?.LIMIT) { - args.push( - 'LIMIT', - options.LIMIT.from.toString(), - options.LIMIT.size.toString() - ); - } - - if (options?.PARAMS) { - pushParamsArgs(args, options.PARAMS); - } - - if (options?.DIALECT) { - args.push('DIALECT', options.DIALECT.toString()); - } - - if (options?.RETURN?.length === 0) { - args.preserve = true; - } - - if (options?.TIMEOUT !== undefined) { - args.push('TIMEOUT', options.TIMEOUT.toString()); - } - - return args; -} - -interface SearchDocumentValue { - [key: string]: string | number | null | Array | SearchDocumentValue; -} - -export interface SearchReply { - total: number; - documents: Array<{ - id: string; - value: SearchDocumentValue; - }>; -} - -export interface ProfileOptions { - LIMITED?: true; -} - -export type ProfileRawReply = [ - results: T, - profile: [ - _: string, - TotalProfileTime: string, - _: string, - ParsingTime: string, - _: string, - PipelineCreationTime: string, - _: string, - IteratorsProfile: Array - ] -]; - -export interface ProfileReply { - results: SearchReply | AGGREGATE.AggregateReply; - profile: ProfileData; -} - -interface ChildIterator { - type?: string, - counter?: number, - term?: string, - size?: number, - time?: string, - childIterators?: Array -} - -interface IteratorsProfile { - type?: string, - counter?: number, - queryType?: string, - time?: string, - childIterators?: Array -} - -interface ProfileData { - totalProfileTime: string, - parsingTime: string, - pipelineCreationTime: string, - iteratorsProfile: IteratorsProfile -} - -export function transformProfile(reply: Array): ProfileData{ - return { - totalProfileTime: reply[0][1], - parsingTime: reply[1][1], - pipelineCreationTime: reply[2][1], - iteratorsProfile: transformIterators(reply[3][1]) - }; -} - -function transformIterators(IteratorsProfile: Array): IteratorsProfile { - var res: IteratorsProfile = {}; - for (let i = 0; i < IteratorsProfile.length; i += 2) { - const value = IteratorsProfile[i+1]; - switch (IteratorsProfile[i]) { - case 'Type': - res.type = value; - break; - case 'Counter': - res.counter = value; - break; - case 'Time': - res.time = value; - break; - case 'Query type': - res.queryType = value; - break; - case 'Child iterators': - res.childIterators = value.map(transformChildIterators); - break; - } - } - - return res; -} - -function transformChildIterators(IteratorsProfile: Array): ChildIterator { - var res: ChildIterator = {}; - for (let i = 1; i < IteratorsProfile.length; i += 2) { - const value = IteratorsProfile[i+1]; - switch (IteratorsProfile[i]) { - case 'Type': - res.type = value; - break; - case 'Counter': - res.counter = value; - break; - case 'Time': - res.time = value; - break; - case 'Size': - res.size = value; - break; - case 'Term': - res.term = value; - break; - case 'Child iterators': - res.childIterators = value.map(transformChildIterators); - break; - } - } - - return res; -} diff --git a/packages/search/lib/index.ts b/packages/search/lib/index.ts index 0f84c11466f..34d57e8ae5e 100644 --- a/packages/search/lib/index.ts +++ b/packages/search/lib/index.ts @@ -1,5 +1,7 @@ export { default } from './commands'; -export { RediSearchSchema, RedisSearchLanguages, SchemaFieldTypes, SchemaTextFieldPhonetics, SearchReply, VectorAlgorithms } from './commands'; -export { AggregateGroupByReducers, AggregateSteps } from './commands/AGGREGATE'; -export { SearchOptions } from './commands/SEARCH'; +export { SCHEMA_FIELD_TYPE, SchemaFieldType } from './commands/CREATE'; + +// export { RediSearchSchema, RedisSearchLanguages, SchemaFieldTypes, SchemaTextFieldPhonetics, SearchReply, VectorAlgorithms } from './commands'; +// export { AggregateGroupByReducers, AggregateSteps } from './commands/AGGREGATE'; +// export { SearchOptions } from './commands/SEARCH'; diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index 9e0af209103..ce43a37bc21 100644 --- a/packages/search/lib/test-utils.ts +++ b/packages/search/lib/test-utils.ts @@ -2,15 +2,15 @@ import TestUtils from '@redis/test-utils'; import RediSearch from '.'; export default new TestUtils({ - dockerImageName: 'redislabs/redisearch', + dockerImageName: 'redis/redis-stack', dockerImageVersionArgument: 'redisearch-version', - defaultDockerVersion: '2.4.9' + defaultDockerVersion: '7.4.0-v1' }); export const GLOBAL = { SERVERS: { OPEN: { - serverArguments: ['--loadmodule /usr/lib/redis/modules/redisearch.so'], + serverArguments: [], clientOptions: { modules: { ft: RediSearch diff --git a/packages/search/package.json b/packages/search/package.json index aaf9bc50f11..ea6c8a6b4c2 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,30 +1,24 @@ { "name": "@redis/search", - "version": "1.2.0", + "version": "2.0.0-next.2", "license": "MIT", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/lib/index.js", + "types": "./dist/lib/index.d.ts", "files": [ - "dist/" + "dist/", + "!dist/tsconfig.tsbuildinfo" ], "scripts": { - "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'", - "build": "tsc", - "documentation": "typedoc" + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" }, "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "repository": { "type": "git", diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index a7e1c610eee..a1cb63eb7bf 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -1,260 +1,256 @@ -import { createConnection } from 'net'; -import { once } from 'events'; -import RedisClient from '@redis/client/dist/lib/client'; -import { promiseTimeout } from '@redis/client/dist/lib/utils'; -import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS'; -import * as path from 'path'; -import { promisify } from 'util'; -import { exec } from 'child_process'; +import { createConnection } from 'node:net'; +import { once } from 'node:events'; +import { createClient } from '@redis/client/index'; +import { setTimeout } from 'node:timers/promises'; +// import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS'; +import { promisify } from 'node:util'; +import { exec } from 'node:child_process'; const execAsync = promisify(exec); interface ErrorWithCode extends Error { - code: string; + code: string; } async function isPortAvailable(port: number): Promise { - try { - const socket = createConnection({ port }); - await once(socket, 'connect'); - socket.end(); - } catch (err) { - if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') { - return true; - } + try { + const socket = createConnection({ port }); + await once(socket, 'connect'); + socket.end(); + } catch (err) { + if (err instanceof Error && (err as ErrorWithCode).code === 'ECONNREFUSED') { + return true; } + } - return false; + return false; } -const portIterator = (async function*(): AsyncIterableIterator { - for (let i = 6379; i < 65535; i++) { - if (await isPortAvailable(i)) { - yield i; - } +const portIterator = (async function* (): AsyncIterableIterator { + for (let i = 6379; i < 65535; i++) { + if (await isPortAvailable(i)) { + yield i; } + } - throw new Error('All ports are in use'); + throw new Error('All ports are in use'); })(); export interface RedisServerDockerConfig { - image: string; - version: string; + image: string; + version: string; } export interface RedisServerDocker { - port: number; - dockerId: string; + port: number; + dockerId: string; } -// ".." cause it'll be in `./dist` -const DOCKER_FODLER_PATH = path.join(__dirname, '../docker'); - async function spawnRedisServerDocker({ image, version }: RedisServerDockerConfig, serverArguments: Array): Promise { - const port = (await portIterator.next()).value, - { stdout, stderr } = await execAsync( - 'docker run -d --network host $(' + - `docker build ${DOCKER_FODLER_PATH} -q ` + - `--build-arg IMAGE=${image}:${version} ` + - `--build-arg REDIS_ARGUMENTS="--save '' --port ${port.toString()} ${serverArguments.join(' ')}"` + - ')' - ); + const port = (await portIterator.next()).value, + { stdout, stderr } = await execAsync( + `docker run -e REDIS_ARGS="--port ${port.toString()} ${serverArguments.join(' ')}" -d --network host ${image}:${version}` + ); - if (!stdout) { - throw new Error(`docker run error - ${stderr}`); - } + if (!stdout) { + throw new Error(`docker run error - ${stderr}`); + } - while (await isPortAvailable(port)) { - await promiseTimeout(50); - } + while (await isPortAvailable(port)) { + await setTimeout(50); + } - return { - port, - dockerId: stdout.trim() - }; + return { + port, + dockerId: stdout.trim() + }; } const RUNNING_SERVERS = new Map, ReturnType>(); export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverArguments: Array): Promise { - const runningServer = RUNNING_SERVERS.get(serverArguments); - if (runningServer) { - return runningServer; - } - - const dockerPromise = spawnRedisServerDocker(dockerConfig, serverArguments); - RUNNING_SERVERS.set(serverArguments, dockerPromise); - return dockerPromise; + const runningServer = RUNNING_SERVERS.get(serverArguments); + if (runningServer) { + return runningServer; + } + + const dockerPromise = spawnRedisServerDocker(dockerConfig, serverArguments); + RUNNING_SERVERS.set(serverArguments, dockerPromise); + return dockerPromise; } async function dockerRemove(dockerId: string): Promise { - const { stderr } = await execAsync(`docker rm -f ${dockerId}`); - if (stderr) { - throw new Error(`docker rm error - ${stderr}`); - } + const { stderr } = await execAsync(`docker rm -f ${dockerId}`); + if (stderr) { + throw new Error(`docker rm error - ${stderr}`); + } } after(() => { - return Promise.all( - [...RUNNING_SERVERS.values()].map(async dockerPromise => - await dockerRemove((await dockerPromise).dockerId) - ) - ); + return Promise.all( + [...RUNNING_SERVERS.values()].map(async dockerPromise => + await dockerRemove((await dockerPromise).dockerId) + ) + ); }); export interface RedisClusterDockersConfig extends RedisServerDockerConfig { - numberOfMasters?: number; - numberOfReplicas?: number; + numberOfMasters?: number; + numberOfReplicas?: number; } async function spawnRedisClusterNodeDockers( - dockersConfig: RedisClusterDockersConfig, - serverArguments: Array, - fromSlot: number, - toSlot: number + dockersConfig: RedisClusterDockersConfig, + serverArguments: Array, + fromSlot: number, + toSlot: number ) { - const range: Array = []; - for (let i = fromSlot; i < toSlot; i++) { - range.push(i); - } - - const master = await spawnRedisClusterNodeDocker( - dockersConfig, - serverArguments - ); + const range: Array = []; + for (let i = fromSlot; i < toSlot; i++) { + range.push(i); + } + + const master = await spawnRedisClusterNodeDocker( + dockersConfig, + serverArguments + ); + + await master.client.clusterAddSlots(range); + + if (!dockersConfig.numberOfReplicas) return [master]; + + const replicasPromises: Array> = []; + for (let i = 0; i < (dockersConfig.numberOfReplicas ?? 0); i++) { + replicasPromises.push( + spawnRedisClusterNodeDocker(dockersConfig, [ + ...serverArguments, + '--cluster-enabled', + 'yes', + '--cluster-node-timeout', + '5000' + ]).then(async replica => { + await replica.client.clusterMeet('127.0.0.1', master.docker.port); + + while ((await replica.client.clusterSlots()).length === 0) { + await setTimeout(50); + } - await master.client.clusterAddSlots(range); - - if (!dockersConfig.numberOfReplicas) return [master]; - - const replicasPromises: Array> = []; - for (let i = 0; i < (dockersConfig.numberOfReplicas ?? 0); i++) { - replicasPromises.push( - spawnRedisClusterNodeDocker(dockersConfig, [ - ...serverArguments, - '--cluster-enabled', - 'yes', - '--cluster-node-timeout', - '5000' - ]).then(async replica => { - await replica.client.clusterMeet('127.0.0.1', master.docker.port); - - while ((await replica.client.clusterSlots()).length === 0) { - await promiseTimeout(50); - } - - await replica.client.clusterReplicate( - await master.client.clusterMyId() - ); - - return replica; - }) + await replica.client.clusterReplicate( + await master.client.clusterMyId() ); - } - return [ - master, - ...await Promise.all(replicasPromises) - ]; + return replica; + }) + ); + } + + return [ + master, + ...await Promise.all(replicasPromises) + ]; } async function spawnRedisClusterNodeDocker( - dockersConfig: RedisClusterDockersConfig, - serverArguments: Array + dockersConfig: RedisClusterDockersConfig, + serverArguments: Array ) { - const docker = await spawnRedisServerDocker(dockersConfig, [ - ...serverArguments, - '--cluster-enabled', - 'yes', - '--cluster-node-timeout', - '5000' - ]), - client = RedisClient.create({ - socket: { - port: docker.port - } - }); - - await client.connect(); - - return { - docker, - client - }; + const docker = await spawnRedisServerDocker(dockersConfig, [ + ...serverArguments, + '--cluster-enabled', + 'yes', + '--cluster-node-timeout', + '5000' + ]), + client = createClient({ + socket: { + port: docker.port + } + }); + + await client.connect(); + + return { + docker, + client + }; } const SLOTS = 16384; async function spawnRedisClusterDockers( - dockersConfig: RedisClusterDockersConfig, - serverArguments: Array + dockersConfig: RedisClusterDockersConfig, + serverArguments: Array ): Promise> { - const numberOfMasters = dockersConfig.numberOfMasters ?? 2, - slotsPerNode = Math.floor(SLOTS / numberOfMasters), - spawnPromises: Array> = []; - for (let i = 0; i < numberOfMasters; i++) { - const fromSlot = i * slotsPerNode, - toSlot = i === numberOfMasters - 1 ? SLOTS : fromSlot + slotsPerNode; - spawnPromises.push( - spawnRedisClusterNodeDockers( - dockersConfig, - serverArguments, - fromSlot, - toSlot - ) - ); - } + const numberOfMasters = dockersConfig.numberOfMasters ?? 2, + slotsPerNode = Math.floor(SLOTS / numberOfMasters), + spawnPromises: Array> = []; + for (let i = 0; i < numberOfMasters; i++) { + const fromSlot = i * slotsPerNode, + toSlot = i === numberOfMasters - 1 ? SLOTS : fromSlot + slotsPerNode; + spawnPromises.push( + spawnRedisClusterNodeDockers( + dockersConfig, + serverArguments, + fromSlot, + toSlot + ) + ); + } - const nodes = (await Promise.all(spawnPromises)).flat(), - meetPromises: Array> = []; - for (let i = 1; i < nodes.length; i++) { - meetPromises.push( - nodes[i].client.clusterMeet('127.0.0.1', nodes[0].docker.port) - ); - } + const nodes = (await Promise.all(spawnPromises)).flat(), + meetPromises: Array> = []; + for (let i = 1; i < nodes.length; i++) { + meetPromises.push( + nodes[i].client.clusterMeet('127.0.0.1', nodes[0].docker.port) + ); + } - await Promise.all(meetPromises); + await Promise.all(meetPromises); - await Promise.all( - nodes.map(async ({ client }) => { - while (totalNodes(await client.clusterSlots()) !== nodes.length) { - await promiseTimeout(50); - } - - return client.disconnect(); - }) - ); + await Promise.all( + nodes.map(async ({ client }) => { + while ( + totalNodes(await client.clusterSlots()) !== nodes.length || + !(await client.sendCommand(['CLUSTER', 'INFO'])).startsWith('cluster_state:ok') // TODO + ) { + await setTimeout(50); + } + + client.destroy(); + }) + ); - return nodes.map(({ docker }) => docker); + return nodes.map(({ docker }) => docker); } -function totalNodes(slots: ClusterSlotsReply) { - let total = slots.length; - for (const slot of slots) { - total += slot.replicas.length; - } +// TODO: type ClusterSlotsReply +function totalNodes(slots: any) { + let total = slots.length; + for (const slot of slots) { + total += slot.replicas.length; + } - return total; + return total; } const RUNNING_CLUSTERS = new Map, ReturnType>(); export function spawnRedisCluster(dockersConfig: RedisClusterDockersConfig, serverArguments: Array): Promise> { - const runningCluster = RUNNING_CLUSTERS.get(serverArguments); - if (runningCluster) { - return runningCluster; - } - - const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments); - RUNNING_CLUSTERS.set(serverArguments, dockersPromise); - return dockersPromise; + const runningCluster = RUNNING_CLUSTERS.get(serverArguments); + if (runningCluster) { + return runningCluster; + } + + const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments); + RUNNING_CLUSTERS.set(serverArguments, dockersPromise); + return dockersPromise; } after(() => { - return Promise.all( - [...RUNNING_CLUSTERS.values()].map(async dockersPromise => { - return Promise.all( - (await dockersPromise).map(({ dockerId }) => dockerRemove(dockerId)) - ); - }) - ); + return Promise.all( + [...RUNNING_CLUSTERS.values()].map(async dockersPromise => { + return Promise.all( + (await dockersPromise).map(({ dockerId }) => dockerRemove(dockerId)) + ); + }) + ); }); diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index b9195c5717a..87ba34db7ef 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -1,226 +1,339 @@ -import { RedisModules, RedisFunctions, RedisScripts } from '@redis/client/lib/commands'; -import RedisClient, { RedisClientOptions, RedisClientType } from '@redis/client/lib/client'; -import RedisCluster, { RedisClusterOptions, RedisClusterType } from '@redis/client/lib/cluster'; -import { RedisSocketCommonOptions } from '@redis/client/lib/client/socket'; +import { + RedisModules, + RedisFunctions, + RedisScripts, + RespVersions, + TypeMapping, + // CommandPolicies, + createClient, + RedisClientOptions, + RedisClientType, + RedisPoolOptions, + RedisClientPoolType, + createClientPool, + createCluster, + RedisClusterOptions, + RedisClusterType +} from '@redis/client/index'; import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; interface TestUtilsConfig { - dockerImageName: string; - dockerImageVersionArgument: string; - defaultDockerVersion?: string; + dockerImageName: string; + dockerImageVersionArgument: string; + defaultDockerVersion?: string; } interface CommonTestOptions { - minimumDockerVersion?: Array; + serverArguments: Array; + minimumDockerVersion?: Array; } interface ClientTestOptions< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> extends CommonTestOptions { + clientOptions?: Partial>; + disableClientSetup?: boolean; +} + +interface ClientPoolTestOptions< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping > extends CommonTestOptions { - serverArguments: Array; - clientOptions?: Partial, 'socket'> & { socket: RedisSocketCommonOptions }>; - disableClientSetup?: boolean; + clientOptions?: Partial>; + poolOptions?: RedisPoolOptions; } interface ClusterTestOptions< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping + // POLICIES extends CommandPolicies > extends CommonTestOptions { - serverArguments: Array; - clusterConfiguration?: Partial>; - numberOfMasters?: number; - numberOfReplicas?: number; + clusterConfiguration?: Partial>; + numberOfMasters?: number; + numberOfReplicas?: number; +} + +interface AllTestOptions< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping + // POLICIES extends CommandPolicies +> { + client: ClientTestOptions; + cluster: ClusterTestOptions; } interface Version { - string: string; - numbers: Array; + string: string; + numbers: Array; } export default class TestUtils { - static #parseVersionNumber(version: string): Array { - if (version === 'latest' || version === 'edge') return [Infinity]; - - const dashIndex = version.indexOf('-'); - return (dashIndex === -1 ? version : version.substring(0, dashIndex)) - .split('.') - .map(x => { - const value = Number(x); - if (Number.isNaN(value)) { - throw new TypeError(`${version} is not a valid redis version`); - } - - return value; - }); - } + static #parseVersionNumber(version: string): Array { + if (version === 'latest' || version === 'edge') return [Infinity]; - static #getVersion(argumentName: string, defaultVersion = 'latest'): Version { - return yargs(hideBin(process.argv)) - .option(argumentName, { - type: 'string', - default: defaultVersion - }) - .coerce(argumentName, (version: string) => { - return { - string: version, - numbers: TestUtils.#parseVersionNumber(version) - }; - }) - .demandOption(argumentName) - .parseSync()[argumentName]; - } + const dashIndex = version.indexOf('-'); + return (dashIndex === -1 ? version : version.substring(0, dashIndex)) + .split('.') + .map(x => { + const value = Number(x); + if (Number.isNaN(value)) { + throw new TypeError(`${version} is not a valid redis version`); + } - readonly #VERSION_NUMBERS: Array; - readonly #DOCKER_IMAGE: RedisServerDockerConfig; + return value; + }); + } - constructor(config: TestUtilsConfig) { - const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion); - this.#VERSION_NUMBERS = numbers; - this.#DOCKER_IMAGE = { - image: config.dockerImageName, - version: string + static #getVersion(argumentName: string, defaultVersion = 'latest'): Version { + return yargs(hideBin(process.argv)) + .option(argumentName, { + type: 'string', + default: defaultVersion + }) + .coerce(argumentName, (version: string) => { + return { + string: version, + numbers: TestUtils.#parseVersionNumber(version) }; - } + }) + .demandOption(argumentName) + .parseSync()[argumentName]; + } - isVersionGreaterThan(minimumVersion: Array | undefined): boolean { - if (minimumVersion === undefined) return true; + readonly #VERSION_NUMBERS: Array; + readonly #DOCKER_IMAGE: RedisServerDockerConfig; - const lastIndex = Math.min(this.#VERSION_NUMBERS.length, minimumVersion.length) - 1; - for (let i = 0; i < lastIndex; i++) { - if (this.#VERSION_NUMBERS[i] > minimumVersion[i]) { - return true; - } else if (minimumVersion[i] > this.#VERSION_NUMBERS[i]) { - return false; - } - } + constructor(config: TestUtilsConfig) { + const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion); + this.#VERSION_NUMBERS = numbers; + this.#DOCKER_IMAGE = { + image: config.dockerImageName, + version: string + }; + } - return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex]; + isVersionGreaterThan(minimumVersion: Array | undefined): boolean { + if (minimumVersion === undefined) return true; + + const lastIndex = Math.min(this.#VERSION_NUMBERS.length, minimumVersion.length) - 1; + for (let i = 0; i < lastIndex; i++) { + if (this.#VERSION_NUMBERS[i] > minimumVersion[i]) { + return true; + } else if (minimumVersion[i] > this.#VERSION_NUMBERS[i]) { + return false; + } } - isVersionGreaterThanHook(minimumVersion: Array | undefined): void { - const isVersionGreaterThan = this.isVersionGreaterThan.bind(this); - before(function () { - if (!isVersionGreaterThan(minimumVersion)) { - return this.skip(); - } - }); + return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex]; + } + + isVersionGreaterThanHook(minimumVersion: Array | undefined): void { + const isVersionGreaterThan = this.isVersionGreaterThan.bind(this); + before(function () { + if (!isVersionGreaterThan(minimumVersion)) { + return this.skip(); + } + }); + } + + testWithClient< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + >( + title: string, + fn: (client: RedisClientType) => unknown, + options: ClientTestOptions + ): void { + let dockerPromise: ReturnType; + if (this.isVersionGreaterThan(options.minimumDockerVersion)) { + const dockerImage = this.#DOCKER_IMAGE; + before(function () { + this.timeout(30000); + + dockerPromise = spawnRedisServer(dockerImage, options.serverArguments); + return dockerPromise; + }); } - testWithClient< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >( - title: string, - fn: (client: RedisClientType) => unknown, - options: ClientTestOptions - ): void { - let dockerPromise: ReturnType; - if (this.isVersionGreaterThan(options.minimumDockerVersion)) { - const dockerImage = this.#DOCKER_IMAGE; - before(function () { - this.timeout(30000); - - dockerPromise = spawnRedisServer(dockerImage, options.serverArguments); - return dockerPromise; - }); + it(title, async function () { + if (!dockerPromise) return this.skip(); + + const client = createClient({ + ...options.clientOptions, + socket: { + ...options.clientOptions?.socket, + // TODO + // @ts-ignore + port: (await dockerPromise).port } + }); - it(title, async function() { - if (!dockerPromise) return this.skip(); + if (options.disableClientSetup) { + return fn(client); + } - const client = RedisClient.create({ - ...options?.clientOptions, - socket: { - ...options?.clientOptions?.socket, - port: (await dockerPromise).port - } - }); + await client.connect(); - if (options.disableClientSetup) { - return fn(client); - } + try { + await client.flushAll(); + await fn(client); + } finally { + if (client.isOpen) { + await client.flushAll(); + client.destroy(); + } + } + }); + } - await client.connect(); + testWithClientPool< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + >( + title: string, + fn: (client: RedisClientPoolType) => unknown, + options: ClientPoolTestOptions + ): void { + let dockerPromise: ReturnType; + if (this.isVersionGreaterThan(options.minimumDockerVersion)) { + const dockerImage = this.#DOCKER_IMAGE; + before(function () { + this.timeout(30000); - try { - await client.flushAll(); - await fn(client); - } finally { - if (client.isOpen) { - await client.flushAll(); - await client.disconnect(); - } - } - }); + dockerPromise = spawnRedisServer(dockerImage, options.serverArguments); + return dockerPromise; + }); } - static async #clusterFlushAll< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >(cluster: RedisClusterType): Promise { - return Promise.all( - cluster.masters.map(async ({ client }) => { - if (client) { - await (await client).flushAll(); - } - }) - ); - } + it(title, async function () { + if (!dockerPromise) return this.skip(); + + const pool = createClientPool({ + ...options.clientOptions, + socket: { + ...options.clientOptions?.socket, + // TODO + // @ts-ignore + port: (await dockerPromise).port + } + }, options.poolOptions); + + await pool.connect(); - testWithCluster< - M extends RedisModules, - F extends RedisFunctions, - S extends RedisScripts - >( - title: string, - fn: (cluster: RedisClusterType) => unknown, - options: ClusterTestOptions - ): void { - let dockersPromise: ReturnType; - if (this.isVersionGreaterThan(options.minimumDockerVersion)) { - const dockerImage = this.#DOCKER_IMAGE; - before(function () { - this.timeout(30000); - - dockersPromise = spawnRedisCluster({ - ...dockerImage, - numberOfMasters: options?.numberOfMasters, - numberOfReplicas: options?.numberOfReplicas - }, options.serverArguments); - return dockersPromise; - }); + try { + await pool.flushAll(); + await fn(pool); + } finally { + await pool.flushAll(); + pool.destroy(); + } + }); + } + + static async #clusterFlushAll< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping + // POLICIES extends CommandPolicies + >(cluster: RedisClusterType): Promise { + return Promise.all( + cluster.masters.map(async master => { + if (master.client) { + await (await cluster.nodeClient(master)).flushAll(); } + }) + ); + } + + testWithCluster< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} + >( + title: string, + fn: (cluster: RedisClusterType) => unknown, + options: ClusterTestOptions + ): void { + let dockersPromise: ReturnType; + if (this.isVersionGreaterThan(options.minimumDockerVersion)) { + const dockerImage = this.#DOCKER_IMAGE; + before(function () { + this.timeout(30000); + + dockersPromise = spawnRedisCluster({ + ...dockerImage, + numberOfMasters: options.numberOfMasters, + numberOfReplicas: options.numberOfReplicas + }, options.serverArguments); + return dockersPromise; + }); + } - it(title, async function () { - if (!dockersPromise) return this.skip(); - - const dockers = await dockersPromise, - cluster = RedisCluster.create({ - rootNodes: dockers.map(({ port }) => ({ - socket: { - port - } - })), - minimizeConnections: true, - ...options.clusterConfiguration - }); - - await cluster.connect(); - - try { - await TestUtils.#clusterFlushAll(cluster); - await fn(cluster); - } finally { - await TestUtils.#clusterFlushAll(cluster); - await cluster.disconnect(); + it(title, async function () { + if (!dockersPromise) return this.skip(); + + const dockers = await dockersPromise, + cluster = createCluster({ + rootNodes: dockers.map(({ port }) => ({ + socket: { + port } + })), + minimizeConnections: true, + ...options.clusterConfiguration }); - } + + await cluster.connect(); + + try { + await TestUtils.#clusterFlushAll(cluster); + await fn(cluster); + } finally { + await TestUtils.#clusterFlushAll(cluster); + cluster.destroy(); + } + }); + } + + testAll< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + // POLICIES extends CommandPolicies = {} + >( + title: string, + fn: (client: RedisClientType | RedisClusterType) => unknown, + options: AllTestOptions + ) { + this.testWithClient(`client.${title}`, fn, options.client); + this.testWithCluster(`cluster.${title}`, fn, options.cluster); + } } diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 2f4e366536e..5e291211b6c 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -1,24 +1,13 @@ { "name": "@redis/test-utils", "private": true, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "scripts": { - "build": "tsc" - }, + "main": "./dist/lib/index.js", + "types": "./dist/lib/index.d.ts", "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "*" }, "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@types/mocha": "^10.0.1", - "@types/node": "^20.6.2", - "@types/yargs": "^17.0.24", - "mocha": "^10.2.0", - "nyc": "^15.1.0", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typescript": "^5.2.2", + "@types/yargs": "^17.0.32", "yargs": "^17.7.2" } } diff --git a/packages/test-utils/tsconfig.json b/packages/test-utils/tsconfig.json index 14fda1d8711..6bb104668fc 100644 --- a/packages/test-utils/tsconfig.json +++ b/packages/test-utils/tsconfig.json @@ -5,5 +5,8 @@ }, "include": [ "./lib/**/*.ts" - ] + ], + "references": [{ + "path": "../client" + }] } diff --git a/packages/time-series/.npmignore b/packages/time-series/.npmignore deleted file mode 100644 index bbef2b404fb..00000000000 --- a/packages/time-series/.npmignore +++ /dev/null @@ -1,6 +0,0 @@ -.nyc_output/ -coverage/ -lib/ -.nycrc.json -.release-it.json -tsconfig.json diff --git a/packages/time-series/.release-it.json b/packages/time-series/.release-it.json index b5a7c08d24f..6c59e8955cf 100644 --- a/packages/time-series/.release-it.json +++ b/packages/time-series/.release-it.json @@ -5,6 +5,7 @@ "tagAnnotation": "Release ${tagName}" }, "npm": { + "versionArgs": ["--workspaces-update=false"], "publishArgs": ["--access", "public"] } } diff --git a/packages/time-series/README.md b/packages/time-series/README.md index 5923979cd48..ff42bfb6b3d 100644 --- a/packages/time-series/README.md +++ b/packages/time-series/README.md @@ -1,8 +1,10 @@ # @redis/time-series -This package provides support for the [RedisTimeSeries](https://redistimeseries.io) module, which adds a time series data structure to Redis. It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RedisTimeSeries commands. +This package provides support for the [RedisTimeSeries](https://redis.io/docs/data-types/timeseries/) module, which adds a time series data structure to Redis. -To use these extra commands, your Redis server must have the RedisTimeSeries module installed. +Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis). + +:warning: To use these extra commands, your Redis server must have the RedisTimeSeries module installed. ## Usage @@ -20,20 +22,18 @@ import { createClient } from 'redis'; import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding, TimeSeriesAggregationType } from '@redis/time-series'; ... - - const created = await client.ts.create('temperature', { - RETENTION: 86400000, // 1 day in milliseconds - ENCODING: TimeSeriesEncoding.UNCOMPRESSED, // No compression - When not specified, the option is set to COMPRESSED - DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK, // No duplicates - When not specified: set to the global DUPLICATE_POLICY configuration of the database (which by default, is BLOCK). - }); - - if (created === 'OK') { - console.log('Created timeseries.'); - } else { - console.log('Error creating timeseries :('); - process.exit(1); - } - +const created = await client.ts.create('temperature', { + RETENTION: 86400000, // 1 day in milliseconds + ENCODING: TimeSeriesEncoding.UNCOMPRESSED, // No compression - When not specified, the option is set to COMPRESSED + DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK, // No duplicates - When not specified: set to the global DUPLICATE_POLICY configuration of the database (which by default, is BLOCK). +}); + +if (created === 'OK') { + console.log('Created timeseries.'); +} else { + console.log('Error creating timeseries :('); + process.exit(1); +} ``` ### Adding new value to a Time Series data structure in Redis @@ -43,33 +43,31 @@ With RedisTimeSeries, we can add a single value to time series data structure us ```javascript let value = Math.floor(Math.random() * 1000) + 1; // Random data point value - let currentTimestamp = 1640995200000; // Jan 1 2022 00:00:00 - let num = 0; - - while (num < 10000) { - // Add a new value to the timeseries, providing our own timestamp: - // https://redis.io/commands/ts.add/ - await client.ts.add('temperature', currentTimestamp, value); - console.log(`Added timestamp ${currentTimestamp}, value ${value}.`); - - num += 1; - value = Math.floor(Math.random() * 1000) + 1; // Get another random value - currentTimestamp += 1000; // Move on one second. - } - - // Add multiple values to the timeseries in round trip to the server: - // https://redis.io/commands/ts.madd/ - const response = await client.ts.mAdd([{ - key: 'temperature', - timestamp: currentTimestamp + 60000, - value: Math.floor(Math.random() * 1000) + 1 - }, { - key: 'temperature', - timestamp: currentTimestamp + 120000, - value: Math.floor(Math.random() * 1000) + 1 - }]); - - +let currentTimestamp = 1640995200000; // Jan 1 2022 00:00:00 +let num = 0; + +while (num < 10000) { + // Add a new value to the timeseries, providing our own timestamp: + // https://redis.io/commands/ts.add/ + await client.ts.add('temperature', currentTimestamp, value); + console.log(`Added timestamp ${currentTimestamp}, value ${value}.`); + + num += 1; + value = Math.floor(Math.random() * 1000) + 1; // Get another random value + currentTimestamp += 1000; // Move on one second. +} + +// Add multiple values to the timeseries in round trip to the server: +// https://redis.io/commands/ts.madd/ +const response = await client.ts.mAdd([{ + key: 'temperature', + timestamp: currentTimestamp + 60000, + value: Math.floor(Math.random() * 1000) + 1 +}, { + key: 'temperature', + timestamp: currentTimestamp + 120000, + value: Math.floor(Math.random() * 1000) + 1 +}]); ``` ### Retrieving Time Series data from Redis @@ -77,31 +75,29 @@ let value = Math.floor(Math.random() * 1000) + 1; // Random data point value With RedisTimeSeries, we can retrieve the time series data using the [`TS.RANGE`](https://redis.io/commands/ts.range/) command by passing the criteria as follows: ```javascript - // Query the timeseries with TS.RANGE: - // https://redis.io/commands/ts.range/ - const fromTimestamp = 1640995200000; // Jan 1 2022 00:00:00 - const toTimestamp = 1640995260000; // Jan 1 2022 00:01:00 - const rangeResponse = await client.ts.range('temperature', fromTimestamp, toTimestamp, { - // Group into 10 second averages. - AGGREGATION: { - type: TimeSeriesAggregationType.AVERAGE, - timeBucket: 10000 - } - }); - - console.log('RANGE RESPONSE:'); - // rangeResponse looks like: - // [ - // { timestamp: 1640995200000, value: 356.8 }, - // { timestamp: 1640995210000, value: 534.8 }, - // { timestamp: 1640995220000, value: 481.3 }, - // { timestamp: 1640995230000, value: 437 }, - // { timestamp: 1640995240000, value: 507.3 }, - // { timestamp: 1640995250000, value: 581.2 }, - // { timestamp: 1640995260000, value: 600 } - // ] - +// https://redis.io/commands/ts.range/ +const fromTimestamp = 1640995200000; // Jan 1 2022 00:00:00 +const toTimestamp = 1640995260000; // Jan 1 2022 00:01:00 +const rangeResponse = await client.ts.range('temperature', fromTimestamp, toTimestamp, { + // Group into 10 second averages. + AGGREGATION: { + type: TimeSeriesAggregationType.AVERAGE, + timeBucket: 10000 + } +}); + +console.log('RANGE RESPONSE:'); +// rangeResponse looks like: +// [ +// { timestamp: 1640995200000, value: 356.8 }, +// { timestamp: 1640995210000, value: 534.8 }, +// { timestamp: 1640995220000, value: 481.3 }, +// { timestamp: 1640995230000, value: 437 }, +// { timestamp: 1640995240000, value: 507.3 }, +// { timestamp: 1640995250000, value: 581.2 }, +// { timestamp: 1640995260000, value: 600 } +// ] ``` ### Altering Time Series data Stored in Redis @@ -111,12 +107,10 @@ RedisTimeSeries includes commands that can update values in a time series data s Using the [`TS.ALTER`](https://redis.io/commands/ts.alter/) command, we can update time series retention like this: ```javascript - - // https://redis.io/commands/ts.alter/ - const alterResponse = await client.ts.alter('temperature', { - RETENTION: 0 // Keep the entries forever - }); - +// https://redis.io/commands/ts.alter/ +const alterResponse = await client.ts.alter('temperature', { + RETENTION: 0 // Keep the entries forever +}); ``` ### Retrieving Information about the timeseries Stored in Redis @@ -126,26 +120,24 @@ RedisTimeSeries also includes commands that can help to view the information on Using the [`TS.INFO`](https://redis.io/commands/ts.info/) command, we can view timeseries information like this: ```javascript - - // Get some information about the state of the timeseries. - // https://redis.io/commands/ts.info/ - const tsInfo = await client.ts.info('temperature'); - - // tsInfo looks like this: - // { - // totalSamples: 1440, - // memoryUsage: 28904, - // firstTimestamp: 1641508920000, - // lastTimestamp: 1641595320000, - // retentionTime: 86400000, - // chunkCount: 7, - // chunkSize: 4096, - // chunkType: 'uncompressed', - // duplicatePolicy: 'block', - // labels: [], - // sourceKey: null, - // rules: [] - // } - +// Get some information about the state of the timeseries. +// https://redis.io/commands/ts.info/ +const tsInfo = await client.ts.info('temperature'); + +// tsInfo looks like this: +// { +// totalSamples: 1440, +// memoryUsage: 28904, +// firstTimestamp: 1641508920000, +// lastTimestamp: 1641595320000, +// retentionTime: 86400000, +// chunkCount: 7, +// chunkSize: 4096, +// chunkType: 'uncompressed', +// duplicatePolicy: 'block', +// labels: [], +// sourceKey: null, +// rules: [] +// } ``` diff --git a/packages/time-series/lib/commands/ADD.spec.ts b/packages/time-series/lib/commands/ADD.spec.ts index 07e67c1adec..7dcf031c2b2 100644 --- a/packages/time-series/lib/commands/ADD.spec.ts +++ b/packages/time-series/lib/commands/ADD.spec.ts @@ -1,90 +1,93 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ADD'; -import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding } from '.'; +import ADD from './ADD'; +import { TIME_SERIES_ENCODING, TIME_SERIES_DUPLICATE_POLICIES } from '.'; -describe('ADD', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key', '*', 1), - ['TS.ADD', 'key', '*', '1'] - ); - }); +describe('TS.ADD', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + ADD.transformArguments('key', '*', 1), + ['TS.ADD', 'key', '*', '1'] + ); + }); - it('with RETENTION', () => { - assert.deepEqual( - transformArguments('key', '*', 1, { - RETENTION: 1 - }), - ['TS.ADD', 'key', '*', '1', 'RETENTION', '1'] - ); - }); + it('with RETENTION', () => { + assert.deepEqual( + ADD.transformArguments('key', '*', 1, { + RETENTION: 1 + }), + ['TS.ADD', 'key', '*', '1', 'RETENTION', '1'] + ); + }); - it('with ENCODING', () => { - assert.deepEqual( - transformArguments('key', '*', 1, { - ENCODING: TimeSeriesEncoding.UNCOMPRESSED - }), - ['TS.ADD', 'key', '*', '1', 'ENCODING', 'UNCOMPRESSED'] - ); - }); + it('with ENCODING', () => { + assert.deepEqual( + ADD.transformArguments('key', '*', 1, { + ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED + }), + ['TS.ADD', 'key', '*', '1', 'ENCODING', 'UNCOMPRESSED'] + ); + }); - it('with CHUNK_SIZE', () => { - assert.deepEqual( - transformArguments('key', '*', 1, { - CHUNK_SIZE: 1 - }), - ['TS.ADD', 'key', '*', '1', 'CHUNK_SIZE', '1'] - ); - }); + it('with CHUNK_SIZE', () => { + assert.deepEqual( + ADD.transformArguments('key', '*', 1, { + CHUNK_SIZE: 1 + }), + ['TS.ADD', 'key', '*', '1', 'CHUNK_SIZE', '1'] + ); + }); - it('with ON_DUPLICATE', () => { - assert.deepEqual( - transformArguments('key', '*', 1, { - ON_DUPLICATE: TimeSeriesDuplicatePolicies.BLOCK - }), - ['TS.ADD', 'key', '*', '1', 'ON_DUPLICATE', 'BLOCK'] - ); - }); + it('with ON_DUPLICATE', () => { + assert.deepEqual( + ADD.transformArguments('key', '*', 1, { + ON_DUPLICATE: TIME_SERIES_DUPLICATE_POLICIES.BLOCK + }), + ['TS.ADD', 'key', '*', '1', 'ON_DUPLICATE', 'BLOCK'] + ); + }); - it('with LABELS', () => { - assert.deepEqual( - transformArguments('key', '*', 1, { - LABELS: { label: 'value' } - }), - ['TS.ADD', 'key', '*', '1', 'LABELS', 'label', 'value'] - ); - }); + it('with LABELS', () => { + assert.deepEqual( + ADD.transformArguments('key', '*', 1, { + LABELS: { label: 'value' } + }), + ['TS.ADD', 'key', '*', '1', 'LABELS', 'label', 'value'] + ); + }); - it('with IGNORE', () => { - assert.deepEqual( - transformArguments('key', '*', 1, { - IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} - }), - ['TS.ADD', 'key', '*', '1', 'IGNORE', '1', '1'] - ) - }); + it ('with IGNORE', () => { + assert.deepEqual( + ADD.transformArguments('key', '*', 1, { + IGNORE: { + maxTimeDiff: 1, + maxValDiff: 1 + } + }), + ['TS.ADD', 'key', '*', '1', 'IGNORE', '1', '1'] + ) + }); - it('with RETENTION, ENCODING, CHUNK_SIZE, ON_DUPLICATE, LABELS, IGNORE', () => { - assert.deepEqual( - transformArguments('key', '*', 1, { - RETENTION: 1, - ENCODING: TimeSeriesEncoding.UNCOMPRESSED, - CHUNK_SIZE: 1, - ON_DUPLICATE: TimeSeriesDuplicatePolicies.BLOCK, - LABELS: { label: 'value' }, - IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} - }), - ['TS.ADD', 'key', '*', '1', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'ON_DUPLICATE', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] - ); - }); + it('with RETENTION, ENCODING, CHUNK_SIZE, ON_DUPLICATE, LABELS, IGNORE', () => { + assert.deepEqual( + ADD.transformArguments('key', '*', 1, { + RETENTION: 1, + ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED, + CHUNK_SIZE: 1, + ON_DUPLICATE: TIME_SERIES_DUPLICATE_POLICIES.BLOCK, + LABELS: { label: 'value' }, + IGNORE: { maxTimeDiff: 1, maxValDiff: 1} + }), + ['TS.ADD', 'key', '*', '1', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'ON_DUPLICATE', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] + ); }); + }); - testUtils.testWithClient('client.ts.add', async client => { - assert.equal( - await client.ts.add('key', 0, 1), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ts.add', async client => { + assert.equal( + await client.ts.add('key', 0, 1), + 0 + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/ADD.ts b/packages/time-series/lib/commands/ADD.ts index 3ed185b9b75..1842dcfc346 100644 --- a/packages/time-series/lib/commands/ADD.ts +++ b/packages/time-series/lib/commands/ADD.ts @@ -1,38 +1,45 @@ +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; import { - transformTimestampArgument, - pushRetentionArgument, - TimeSeriesEncoding, - pushEncodingArgument, - pushChunkSizeArgument, - TimeSeriesDuplicatePolicies, - Labels, - pushLabelsArgument, - Timestamp, - pushIgnoreArgument, + transformTimestampArgument, + pushRetentionArgument, + TimeSeriesEncoding, + pushEncodingArgument, + pushChunkSizeArgument, + TimeSeriesDuplicatePolicies, + Labels, + pushLabelsArgument, + Timestamp, + pushIgnoreArgument } from '.'; export interface TsIgnoreOptions { - MAX_TIME_DIFF: number; - MAX_VAL_DIFF: number; + maxTimeDiff: number; + maxValDiff: number; } -interface AddOptions { - RETENTION?: number; - ENCODING?: TimeSeriesEncoding; - CHUNK_SIZE?: number; - ON_DUPLICATE?: TimeSeriesDuplicatePolicies; - LABELS?: Labels; - IGNORE?: TsIgnoreOptions; +export interface TsAddOptions { + RETENTION?: number; + ENCODING?: TimeSeriesEncoding; + CHUNK_SIZE?: number; + ON_DUPLICATE?: TimeSeriesDuplicatePolicies; + LABELS?: Labels; + IGNORE?: TsIgnoreOptions; } -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: string, timestamp: Timestamp, value: number, options?: AddOptions): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + key: RedisArgument, + timestamp: Timestamp, + value: number, + options?: TsAddOptions + ) { const args = [ - 'TS.ADD', - key, - transformTimestampArgument(timestamp), - value.toString() + 'TS.ADD', + key, + transformTimestampArgument(timestamp), + value.toString() ]; pushRetentionArgument(args, options?.RETENTION); @@ -42,7 +49,7 @@ export function transformArguments(key: string, timestamp: Timestamp, value: num pushChunkSizeArgument(args, options?.CHUNK_SIZE); if (options?.ON_DUPLICATE) { - args.push('ON_DUPLICATE', options.ON_DUPLICATE); + args.push('ON_DUPLICATE', options.ON_DUPLICATE); } pushLabelsArgument(args, options?.LABELS); @@ -50,6 +57,6 @@ export function transformArguments(key: string, timestamp: Timestamp, value: num pushIgnoreArgument(args, options?.IGNORE); return args; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/ALTER.spec.ts b/packages/time-series/lib/commands/ALTER.spec.ts index 7add3eeec3a..1b24111156b 100644 --- a/packages/time-series/lib/commands/ALTER.spec.ts +++ b/packages/time-series/lib/commands/ALTER.spec.ts @@ -1,82 +1,85 @@ -import { strict as assert } from 'assert'; -import { TimeSeriesDuplicatePolicies } from '.'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './ALTER'; +import ALTER from './ALTER'; +import { TIME_SERIES_DUPLICATE_POLICIES } from '.'; -describe('ALTER', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key'), - ['TS.ALTER', 'key'] - ); - }); +describe('TS.ALTER', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + ALTER.transformArguments('key'), + ['TS.ALTER', 'key'] + ); + }); - it('with RETENTION', () => { - assert.deepEqual( - transformArguments('key', { - RETENTION: 1 - }), - ['TS.ALTER', 'key', 'RETENTION', '1'] - ); - }); + it('with RETENTION', () => { + assert.deepEqual( + ALTER.transformArguments('key', { + RETENTION: 1 + }), + ['TS.ALTER', 'key', 'RETENTION', '1'] + ); + }); - it('with CHUNK_SIZE', () => { - assert.deepEqual( - transformArguments('key', { - CHUNK_SIZE: 1 - }), - ['TS.ALTER', 'key', 'CHUNK_SIZE', '1'] - ); - }); + it('with CHUNK_SIZE', () => { + assert.deepEqual( + ALTER.transformArguments('key', { + CHUNK_SIZE: 1 + }), + ['TS.ALTER', 'key', 'CHUNK_SIZE', '1'] + ); + }); - it('with DUPLICATE_POLICY', () => { - assert.deepEqual( - transformArguments('key', { - DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK - }), - ['TS.ALTER', 'key', 'DUPLICATE_POLICY', 'BLOCK'] - ); - }); + it('with DUPLICATE_POLICY', () => { + assert.deepEqual( + ALTER.transformArguments('key', { + DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK + }), + ['TS.ALTER', 'key', 'DUPLICATE_POLICY', 'BLOCK'] + ); + }); - it('with LABELS', () => { - assert.deepEqual( - transformArguments('key', { - LABELS: { label: 'value' } - }), - ['TS.ALTER', 'key', 'LABELS', 'label', 'value'] - ); - }); + it('with LABELS', () => { + assert.deepEqual( + ALTER.transformArguments('key', { + LABELS: { label: 'value' } + }), + ['TS.ALTER', 'key', 'LABELS', 'label', 'value'] + ); + }); - it('with IGNORE with MAX_TIME_DIFF', () => { - assert.deepEqual( - transformArguments('key', { - IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} - }), - ['TS.ALTER', 'key', 'IGNORE', '1', '1'] - ) - }); + it('with IGNORE with MAX_TIME_DIFF', () => { + assert.deepEqual( + ALTER.transformArguments('key', { + IGNORE: { + maxTimeDiff: 1, + maxValDiff: 1 + } + }), + ['TS.ALTER', 'key', 'IGNORE', '1', '1'] + ) + }); - it('with RETENTION, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => { - assert.deepEqual( - transformArguments('key', { - RETENTION: 1, - CHUNK_SIZE: 1, - DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK, - LABELS: { label: 'value' }, - IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} - }), - ['TS.ALTER', 'key', 'RETENTION', '1', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] - ); - }); + it('with RETENTION, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => { + assert.deepEqual( + ALTER.transformArguments('key', { + RETENTION: 1, + CHUNK_SIZE: 1, + DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK, + LABELS: { label: 'value' }, + IGNORE: { maxTimeDiff: 1, maxValDiff: 1} + }), + ['TS.ALTER', 'key', 'RETENTION', '1', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] + ); }); + }); - testUtils.testWithClient('client.ts.alter', async client => { - await client.ts.create('key'); + testUtils.testWithClient('client.ts.alter', async client => { + const [, reply] = await Promise.all([ + client.ts.create('key'), + client.ts.alter('key') + ]); - assert.equal( - await client.ts.alter('key', { RETENTION: 1 }), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/ALTER.ts b/packages/time-series/lib/commands/ALTER.ts index 576153a0cca..f77edb5c43f 100644 --- a/packages/time-series/lib/commands/ALTER.ts +++ b/packages/time-series/lib/commands/ALTER.ts @@ -1,17 +1,13 @@ -import { pushRetentionArgument, Labels, pushLabelsArgument, TimeSeriesDuplicatePolicies, pushChunkSizeArgument, pushDuplicatePolicy, pushIgnoreArgument } from '.'; -import { TsIgnoreOptions } from './ADD'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { TsCreateOptions } from './CREATE'; +import { pushRetentionArgument, pushChunkSizeArgument, pushDuplicatePolicy, pushLabelsArgument, pushIgnoreArgument } from '.'; -export const FIRST_KEY_INDEX = 1; +export type TsAlterOptions = Pick; -interface AlterOptions { - RETENTION?: number; - CHUNK_SIZE?: number; - DUPLICATE_POLICY?: TimeSeriesDuplicatePolicies; - LABELS?: Labels; - IGNORE?: TsIgnoreOptions; -} - -export function transformArguments(key: string, options?: AlterOptions): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: TsAlterOptions) { const args = ['TS.ALTER', key]; pushRetentionArgument(args, options?.RETENTION); @@ -25,6 +21,6 @@ export function transformArguments(key: string, options?: AlterOptions): Array SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/CREATE.spec.ts b/packages/time-series/lib/commands/CREATE.spec.ts index eb7a1c6a637..feff9cbdd7b 100644 --- a/packages/time-series/lib/commands/CREATE.spec.ts +++ b/packages/time-series/lib/commands/CREATE.spec.ts @@ -1,90 +1,93 @@ -import { strict as assert } from 'assert'; -import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding } from '.'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CREATE'; +import CREATE from './CREATE'; +import { TIME_SERIES_ENCODING, TIME_SERIES_DUPLICATE_POLICIES } from '.'; -describe('CREATE', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key'), - ['TS.CREATE', 'key'] - ); - }); +describe('TS.CREATE', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + CREATE.transformArguments('key'), + ['TS.CREATE', 'key'] + ); + }); + + it('with RETENTION', () => { + assert.deepEqual( + CREATE.transformArguments('key', { + RETENTION: 1 + }), + ['TS.CREATE', 'key', 'RETENTION', '1'] + ); + }); - it('with RETENTION', () => { - assert.deepEqual( - transformArguments('key', { - RETENTION: 1 - }), - ['TS.CREATE', 'key', 'RETENTION', '1'] - ); - }); + it('with ENCODING', () => { + assert.deepEqual( + CREATE.transformArguments('key', { + ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED + }), + ['TS.CREATE', 'key', 'ENCODING', 'UNCOMPRESSED'] + ); + }); - it('with ENCODING', () => { - assert.deepEqual( - transformArguments('key', { - ENCODING: TimeSeriesEncoding.UNCOMPRESSED - }), - ['TS.CREATE', 'key', 'ENCODING', 'UNCOMPRESSED'] - ); - }); + it('with CHUNK_SIZE', () => { + assert.deepEqual( + CREATE.transformArguments('key', { + CHUNK_SIZE: 1 + }), + ['TS.CREATE', 'key', 'CHUNK_SIZE', '1'] + ); + }); - it('with CHUNK_SIZE', () => { - assert.deepEqual( - transformArguments('key', { - CHUNK_SIZE: 1 - }), - ['TS.CREATE', 'key', 'CHUNK_SIZE', '1'] - ); - }); + it('with DUPLICATE_POLICY', () => { + assert.deepEqual( + CREATE.transformArguments('key', { + DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK + }), + ['TS.CREATE', 'key', 'DUPLICATE_POLICY', 'BLOCK'] + ); + }); - it('with DUPLICATE_POLICY', () => { - assert.deepEqual( - transformArguments('key', { - DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK - }), - ['TS.CREATE', 'key', 'DUPLICATE_POLICY', 'BLOCK'] - ); - }); + it('with LABELS', () => { + assert.deepEqual( + CREATE.transformArguments('key', { + LABELS: { label: 'value' } + }), + ['TS.CREATE', 'key', 'LABELS', 'label', 'value'] + ); + }); - it('with LABELS', () => { - assert.deepEqual( - transformArguments('key', { - LABELS: { label: 'value' } - }), - ['TS.CREATE', 'key', 'LABELS', 'label', 'value'] - ); - }); - - it('with IGNORE with MAX_TIME_DIFF', () => { - assert.deepEqual( - transformArguments('key', { - IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} - }), - ['TS.CREATE', 'key', 'IGNORE', '1', '1'] - ) - }); + it('with IGNORE with MAX_TIME_DIFF', () => { + assert.deepEqual( + CREATE.transformArguments('key', { + IGNORE: { + maxTimeDiff: 1, + maxValDiff: 1 + } + }), + ['TS.CREATE', 'key', 'IGNORE', '1', '1'] + ) + }); - it('with RETENTION, ENCODING, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => { - assert.deepEqual( - transformArguments('key', { - RETENTION: 1, - ENCODING: TimeSeriesEncoding.UNCOMPRESSED, - CHUNK_SIZE: 1, - DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK, - LABELS: { label: 'value' }, - IGNORE: { MAX_TIME_DIFF: 1, MAX_VAL_DIFF: 1} - }), - ['TS.CREATE', 'key', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] - ); - }); + it('with RETENTION, ENCODING, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => { + assert.deepEqual( + CREATE.transformArguments('key', { + RETENTION: 1, + ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED, + CHUNK_SIZE: 1, + DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK, + LABELS: { label: 'value' }, + IGNORE: { maxTimeDiff: 1, maxValDiff: 1} + }), + ['TS.CREATE', 'key', 'RETENTION', '1', 'ENCODING', 'UNCOMPRESSED', 'CHUNK_SIZE', '1', 'DUPLICATE_POLICY', 'BLOCK', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] + ); }); + }); - testUtils.testWithClient('client.ts.create', async client => { - assert.equal( - await client.ts.create('key'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ts.create', async client => { + assert.equal( + await client.ts.create('key'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/CREATE.ts b/packages/time-series/lib/commands/CREATE.ts index a84d4b5f9fb..abb84de12a2 100644 --- a/packages/time-series/lib/commands/CREATE.ts +++ b/packages/time-series/lib/commands/CREATE.ts @@ -1,28 +1,30 @@ +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { - pushRetentionArgument, - TimeSeriesEncoding, - pushEncodingArgument, - pushChunkSizeArgument, - TimeSeriesDuplicatePolicies, - Labels, - pushLabelsArgument, - pushDuplicatePolicy, - pushIgnoreArgument + pushRetentionArgument, + TimeSeriesEncoding, + pushEncodingArgument, + pushChunkSizeArgument, + TimeSeriesDuplicatePolicies, + pushDuplicatePolicy, + Labels, + pushLabelsArgument, + pushIgnoreArgument } from '.'; import { TsIgnoreOptions } from './ADD'; -export const FIRST_KEY_INDEX = 1; - -interface CreateOptions { - RETENTION?: number; - ENCODING?: TimeSeriesEncoding; - CHUNK_SIZE?: number; - DUPLICATE_POLICY?: TimeSeriesDuplicatePolicies; - LABELS?: Labels; - IGNORE?: TsIgnoreOptions; +export interface TsCreateOptions { + RETENTION?: number; + ENCODING?: TimeSeriesEncoding; + CHUNK_SIZE?: number; + DUPLICATE_POLICY?: TimeSeriesDuplicatePolicies; + LABELS?: Labels; + IGNORE?: TsIgnoreOptions; } -export function transformArguments(key: string, options?: CreateOptions): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, options?: TsCreateOptions) { const args = ['TS.CREATE', key]; pushRetentionArgument(args, options?.RETENTION); @@ -38,6 +40,6 @@ export function transformArguments(key: string, options?: CreateOptions): Array< pushIgnoreArgument(args, options?.IGNORE); return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/CREATERULE.spec.ts b/packages/time-series/lib/commands/CREATERULE.spec.ts index 65457898181..f1e5b934506 100644 --- a/packages/time-series/lib/commands/CREATERULE.spec.ts +++ b/packages/time-series/lib/commands/CREATERULE.spec.ts @@ -1,34 +1,31 @@ -import { strict as assert } from 'assert'; -import { TimeSeriesAggregationType } from '.'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './CREATERULE'; +import CREATERULE, { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; -describe('CREATERULE', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('source', 'destination', TimeSeriesAggregationType.AVERAGE, 1), - ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1'] - ); - }); +describe('TS.CREATERULE', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + CREATERULE.transformArguments('source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1), + ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1'] + ); + }); - it('with alignTimestamp', () => { - assert.deepEqual( - transformArguments('source', 'destination', TimeSeriesAggregationType.AVERAGE, 1, 1), - ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1', '1'] - ); - }); + it('with alignTimestamp', () => { + assert.deepEqual( + CREATERULE.transformArguments('source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1, 1), + ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1', '1'] + ); }); + }); - testUtils.testWithClient('client.ts.createRule', async client => { - await Promise.all([ - client.ts.create('source'), - client.ts.create('destination') - ]); + testUtils.testWithClient('client.ts.createRule', async client => { + const [, , reply] = await Promise.all([ + client.ts.create('source'), + client.ts.create('destination'), + client.ts.createRule('source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1) + ]); - assert.equal( - await client.ts.createRule('source', 'destination', TimeSeriesAggregationType.AVERAGE, 1), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/CREATERULE.ts b/packages/time-series/lib/commands/CREATERULE.ts index 87b8579a6ee..bd074d7107c 100644 --- a/packages/time-series/lib/commands/CREATERULE.ts +++ b/packages/time-series/lib/commands/CREATERULE.ts @@ -1,28 +1,47 @@ -import { TimeSeriesAggregationType } from '.'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; +export const TIME_SERIES_AGGREGATION_TYPE = { + AVG: 'AVG', + FIRST: 'FIRST', + LAST: 'LAST', + MIN: 'MIN', + MAX: 'MAX', + SUM: 'SUM', + RANGE: 'RANGE', + COUNT: 'COUNT', + STD_P: 'STD.P', + STD_S: 'STD.S', + VAR_P: 'VAR.P', + VAR_S: 'VAR.S', + TWA: 'TWA' +} as const; -export function transformArguments( - sourceKey: string, - destinationKey: string, +export type TimeSeriesAggregationType = typeof TIME_SERIES_AGGREGATION_TYPE[keyof typeof TIME_SERIES_AGGREGATION_TYPE]; + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments( + sourceKey: RedisArgument, + destinationKey: RedisArgument, aggregationType: TimeSeriesAggregationType, bucketDuration: number, alignTimestamp?: number -): Array { + ) { const args = [ - 'TS.CREATERULE', - sourceKey, - destinationKey, - 'AGGREGATION', - aggregationType, - bucketDuration.toString() + 'TS.CREATERULE', + sourceKey, + destinationKey, + 'AGGREGATION', + aggregationType, + bucketDuration.toString() ]; - if (alignTimestamp) { - args.push(alignTimestamp.toString()); + if (alignTimestamp !== undefined) { + args.push(alignTimestamp.toString()); } return args; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/DECRBY.spec.ts b/packages/time-series/lib/commands/DECRBY.spec.ts index 345e651404b..dbce98b2acd 100644 --- a/packages/time-series/lib/commands/DECRBY.spec.ts +++ b/packages/time-series/lib/commands/DECRBY.spec.ts @@ -1,81 +1,92 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DECRBY'; +import DECRBY from './DECRBY'; -describe('DECRBY', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key', 1), - ['TS.DECRBY', 'key', '1'] - ); - }); +describe('TS.DECRBY', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + DECRBY.transformArguments('key', 1), + ['TS.DECRBY', 'key', '1'] + ); + }); - it('with TIMESTAMP', () => { - assert.deepEqual( - transformArguments('key', 1, { - TIMESTAMP: '*' - }), - ['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*'] - ); - }); + it('with TIMESTAMP', () => { + assert.deepEqual( + DECRBY.transformArguments('key', 1, { + TIMESTAMP: '*' + }), + ['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*'] + ); + }); - it('with RETENTION', () => { - assert.deepEqual( - transformArguments('key', 1, { - RETENTION: 1 - }), - ['TS.DECRBY', 'key', '1', 'RETENTION', '1'] - ); - }); + it('with RETENTION', () => { + assert.deepEqual( + DECRBY.transformArguments('key', 1, { + RETENTION: 1 + }), + ['TS.DECRBY', 'key', '1', 'RETENTION', '1'] + ); + }); - it('with UNCOMPRESSED', () => { - assert.deepEqual( - transformArguments('key', 1, { - UNCOMPRESSED: true - }), - ['TS.DECRBY', 'key', '1', 'UNCOMPRESSED'] - ); - }); + it('with UNCOMPRESSED', () => { + assert.deepEqual( + DECRBY.transformArguments('key', 1, { + UNCOMPRESSED: true + }), + ['TS.DECRBY', 'key', '1', 'UNCOMPRESSED'] + ); + }); - it('with CHUNK_SIZE', () => { - assert.deepEqual( - transformArguments('key', 1, { - CHUNK_SIZE: 100 - }), - ['TS.DECRBY', 'key', '1', 'CHUNK_SIZE', '100'] - ); - }); + it('with CHUNK_SIZE', () => { + assert.deepEqual( + DECRBY.transformArguments('key', 1, { + CHUNK_SIZE: 100 + }), + ['TS.DECRBY', 'key', '1', 'CHUNK_SIZE', '100'] + ); + }); - it('with LABELS', () => { - assert.deepEqual( - transformArguments('key', 1, { - LABELS: { label: 'value' } - }), - ['TS.DECRBY', 'key', '1', 'LABELS', 'label', 'value'] - ); - }); + it('with LABELS', () => { + assert.deepEqual( + DECRBY.transformArguments('key', 1, { + LABELS: { label: 'value' } + }), + ['TS.DECRBY', 'key', '1', 'LABELS', 'label', 'value'] + ); + }); - it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => { - assert.deepEqual( - transformArguments('key', 1, { - TIMESTAMP: '*', - RETENTION: 1, - UNCOMPRESSED: true, - CHUNK_SIZE: 2, - LABELS: { label: 'value' } - }), - ['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED', 'CHUNK_SIZE', '2', 'LABELS', 'label', 'value'] - ); - }); + it ('with IGNORE', () => { + assert.deepEqual( + DECRBY.transformArguments('key', 1, { + IGNORE: { + maxTimeDiff: 1, + maxValDiff: 1 + } + }), + ['TS.DECRBY', 'key', '1', 'IGNORE', '1', '1'] + ) + }); + + it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => { + assert.deepEqual( + DECRBY.transformArguments('key', 1, { + TIMESTAMP: '*', + RETENTION: 1, + UNCOMPRESSED: true, + CHUNK_SIZE: 2, + LABELS: { label: 'value' }, + IGNORE: { maxTimeDiff: 1, maxValDiff: 1 } + }), + ['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED', 'CHUNK_SIZE', '2', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] + ); }); + }); - testUtils.testWithClient('client.ts.decrBy', async client => { - assert.equal( - await client.ts.decrBy('key', 1, { - TIMESTAMP: 0 - }), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ts.decrBy', async client => { + assert.equal( + typeof await client.ts.decrBy('key', 1), + 'number' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/DECRBY.ts b/packages/time-series/lib/commands/DECRBY.ts index 07b5b6f45c0..a5ee01efb06 100644 --- a/packages/time-series/lib/commands/DECRBY.ts +++ b/packages/time-series/lib/commands/DECRBY.ts @@ -1,10 +1,9 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { IncrDecrOptions, transformIncrDecrArguments } from '.'; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import INCRBY, { transformIncrByArguments } from './INCRBY'; -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: string, value: number, options?: IncrDecrOptions): RedisCommandArguments { - return transformIncrDecrArguments('TS.DECRBY', key, value, options); -} - -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: INCRBY.FIRST_KEY_INDEX, + IS_READ_ONLY: INCRBY.IS_READ_ONLY, + transformArguments: transformIncrByArguments.bind(undefined, 'TS.DECRBY'), + transformReply: INCRBY.transformReply +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/DEL.spec.ts b/packages/time-series/lib/commands/DEL.spec.ts index 0fc4b465807..afe6be77c4b 100644 --- a/packages/time-series/lib/commands/DEL.spec.ts +++ b/packages/time-series/lib/commands/DEL.spec.ts @@ -1,21 +1,21 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DEL'; +import DEL from './DEL'; -describe('DEL', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '-', '+'), - ['TS.DEL', 'key', '-', '+'] - ); - }); +describe('TS.DEL', () => { + it('transformArguments', () => { + assert.deepEqual( + DEL.transformArguments('key', '-', '+'), + ['TS.DEL', 'key', '-', '+'] + ); + }); - testUtils.testWithClient('client.ts.del', async client => { - await client.ts.create('key'); + testUtils.testWithClient('client.ts.del', async client => { + const [, reply] = await Promise.all([ + client.ts.create('key'), + client.ts.del('key', '-', '+') + ]); - assert.equal( - await client.ts.del('key', '-', '+'), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 0); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/DEL.ts b/packages/time-series/lib/commands/DEL.ts index 347954c21de..26c3e610f17 100644 --- a/packages/time-series/lib/commands/DEL.ts +++ b/packages/time-series/lib/commands/DEL.ts @@ -1,15 +1,16 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; import { Timestamp, transformTimestampArgument } from '.'; +import { RedisArgument, NumberReply, Command, } from '@redis/client/dist/lib/RESP/types'; -export const FIRTS_KEY_INDEX = 1; - -export function transformArguments(key: string, fromTimestamp: Timestamp, toTimestamp: Timestamp): RedisCommandArguments { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(key: RedisArgument, fromTimestamp: Timestamp, toTimestamp: Timestamp) { return [ - 'TS.DEL', - key, - transformTimestampArgument(fromTimestamp), - transformTimestampArgument(toTimestamp) + 'TS.DEL', + key, + transformTimestampArgument(fromTimestamp), + transformTimestampArgument(toTimestamp) ]; -} - -export declare function transformReply(): number; + }, + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/DELETERULE.spec.ts b/packages/time-series/lib/commands/DELETERULE.spec.ts index 9364bea711c..8c8568c8567 100644 --- a/packages/time-series/lib/commands/DELETERULE.spec.ts +++ b/packages/time-series/lib/commands/DELETERULE.spec.ts @@ -1,26 +1,24 @@ -import { strict as assert } from 'assert'; -import { TimeSeriesAggregationType } from '.'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './DELETERULE'; +import DELETERULE from './DELETERULE'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; -describe('DELETERULE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('source', 'destination'), - ['TS.DELETERULE', 'source', 'destination'] - ); - }); +describe('TS.DELETERULE', () => { + it('transformArguments', () => { + assert.deepEqual( + DELETERULE.transformArguments('source', 'destination'), + ['TS.DELETERULE', 'source', 'destination'] + ); + }); - testUtils.testWithClient('client.ts.deleteRule', async client => { - await Promise.all([ - client.ts.create('source'), - client.ts.create('destination'), - client.ts.createRule('source', 'destination', TimeSeriesAggregationType.AVERAGE, 1) - ]); + testUtils.testWithClient('client.ts.deleteRule', async client => { + const [, , , reply] = await Promise.all([ + client.ts.create('source'), + client.ts.create('destination'), + client.ts.createRule('source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1), + client.ts.deleteRule('source', 'destination') + ]); - assert.equal( - await client.ts.deleteRule('source', 'destination'), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, 'OK'); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/DELETERULE.ts b/packages/time-series/lib/commands/DELETERULE.ts index 7d2cfaeed94..5cf88897f7d 100644 --- a/packages/time-series/lib/commands/DELETERULE.ts +++ b/packages/time-series/lib/commands/DELETERULE.ts @@ -1,11 +1,14 @@ -export const FIRST_KEY_INDEX = 1; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -export function transformArguments(sourceKey: string, destinationKey: string): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(sourceKey: RedisArgument, destinationKey: RedisArgument) { return [ - 'TS.DELETERULE', - sourceKey, - destinationKey + 'TS.DELETERULE', + sourceKey, + destinationKey ]; -} - -export declare function transformReply(): 'OK'; + }, + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/GET.spec.ts b/packages/time-series/lib/commands/GET.spec.ts index 29634cd775a..a1f47346bc2 100644 --- a/packages/time-series/lib/commands/GET.spec.ts +++ b/packages/time-series/lib/commands/GET.spec.ts @@ -1,46 +1,46 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './GET'; +import GET from './GET'; -describe('GET', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key'), - ['TS.GET', 'key'] - ); - }); +describe('TS.GET', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + GET.transformArguments('key'), + ['TS.GET', 'key'] + ); + }); - it('with LATEST', () => { - assert.deepEqual( - transformArguments('key', { - LATEST: true - }), - ['TS.GET', 'key', 'LATEST'] - ); - }); + it('with LATEST', () => { + assert.deepEqual( + GET.transformArguments('key', { + LATEST: true + }), + ['TS.GET', 'key', 'LATEST'] + ); }); + }); - describe('client.ts.get', () => { - testUtils.testWithClient('null', async client => { - await client.ts.create('key'); + describe('client.ts.get', () => { + testUtils.testWithClient('null', async client => { + const [, reply] = await Promise.all([ + client.ts.create('key'), + client.ts.get('key') + ]); - assert.equal( - await client.ts.get('key'), - null - ); - }, GLOBAL.SERVERS.OPEN); + assert.equal(reply, null); + }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with samples', async client => { - await client.ts.add('key', 0, 1); + testUtils.testWithClient('with sample', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 1), + client.ts.get('key') + ]); - assert.deepEqual( - await client.ts.get('key'), - { - timestamp: 0, - value: 1 - } - ); - }, GLOBAL.SERVERS.OPEN); - }); + assert.deepEqual(reply, { + timestamp: 0, + value: 1 + }); + }, GLOBAL.SERVERS.OPEN); + }); }); diff --git a/packages/time-series/lib/commands/GET.ts b/packages/time-series/lib/commands/GET.ts index 6d74f97c9cd..78e5e3bced0 100644 --- a/packages/time-series/lib/commands/GET.ts +++ b/packages/time-series/lib/commands/GET.ts @@ -1,20 +1,35 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushLatestArgument, SampleRawReply, SampleReply, transformSampleReply } from '.'; +import { RedisArgument, TuplesReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -interface GetOptions { - LATEST?: boolean; +export interface TsGetOptions { + LATEST?: boolean; } -export function transformArguments(key: string, options?: GetOptions): RedisCommandArguments { - return pushLatestArgument(['TS.GET', key], options?.LATEST); -} +export type TsGetReply = TuplesReply<[]> | TuplesReply<[NumberReply, DoubleReply]>; -export function transformReply(reply: [] | SampleRawReply): null | SampleReply { - if (reply.length === 0) return null; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: RedisArgument, options?: TsGetOptions) { + const args = ['TS.GET', key]; + + if (options?.LATEST) { + args.push('LATEST'); + } - return transformSampleReply(reply); -} + return args; + }, + transformReply: { + 2(reply: UnwrapReply>) { + return reply.length === 0 ? null : { + timestamp: reply[0], + value: Number(reply[1]) + }; + }, + 3(reply: UnwrapReply) { + return reply.length === 0 ? null : { + timestamp: reply[0], + value: reply[1] + }; + } + } +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/INCRBY.spec.ts b/packages/time-series/lib/commands/INCRBY.spec.ts index acaa4cd3329..33163a72c82 100644 --- a/packages/time-series/lib/commands/INCRBY.spec.ts +++ b/packages/time-series/lib/commands/INCRBY.spec.ts @@ -1,91 +1,102 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './INCRBY'; +import INCRBY from './INCRBY'; -describe('INCRBY', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key', 1), - ['TS.INCRBY', 'key', '1'] - ); - }); +describe('TS.INCRBY', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1), + ['TS.INCRBY', 'key', '1'] + ); + }); - it('with TIMESTAMP', () => { - assert.deepEqual( - transformArguments('key', 1, { - TIMESTAMP: '*' - }), - ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*'] - ); - }); + it('with TIMESTAMP', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1, { + TIMESTAMP: '*' + }), + ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*'] + ); + }); - it('with RETENTION', () => { - assert.deepEqual( - transformArguments('key', 1, { - RETENTION: 1 - }), - ['TS.INCRBY', 'key', '1', 'RETENTION', '1'] - ); - }); + it('with RETENTION', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1, { + RETENTION: 1 + }), + ['TS.INCRBY', 'key', '1', 'RETENTION', '1'] + ); + }); - it('with UNCOMPRESSED', () => { - assert.deepEqual( - transformArguments('key', 1, { - UNCOMPRESSED: true - }), - ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED'] - ); - }); + it('with UNCOMPRESSED', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1, { + UNCOMPRESSED: true + }), + ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED'] + ); + }); - it('without UNCOMPRESSED', () => { - assert.deepEqual( - transformArguments('key', 1, { - UNCOMPRESSED: false - }), - ['TS.INCRBY', 'key', '1'] - ); - }); + it('without UNCOMPRESSED', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1, { + UNCOMPRESSED: false + }), + ['TS.INCRBY', 'key', '1'] + ); + }); - it('with CHUNK_SIZE', () => { - assert.deepEqual( - transformArguments('key', 1, { - CHUNK_SIZE: 1 - }), - ['TS.INCRBY', 'key', '1', 'CHUNK_SIZE', '1'] - ); - }); + it('with CHUNK_SIZE', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1, { + CHUNK_SIZE: 1 + }), + ['TS.INCRBY', 'key', '1', 'CHUNK_SIZE', '1'] + ); + }); - it('with LABELS', () => { - assert.deepEqual( - transformArguments('key', 1, { - LABELS: { label: 'value' } - }), - ['TS.INCRBY', 'key', '1', 'LABELS', 'label', 'value'] - ); - }); + it('with LABELS', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1, { + LABELS: { label: 'value' } + }), + ['TS.INCRBY', 'key', '1', 'LABELS', 'label', 'value'] + ); + }); - it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => { - assert.deepEqual( - transformArguments('key', 1, { - TIMESTAMP: '*', - RETENTION: 1, - UNCOMPRESSED: true, - CHUNK_SIZE: 1, - LABELS: { label: 'value' } - }), - ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED', - 'CHUNK_SIZE', '1', 'LABELS', 'label', 'value'] - ); - }); + it ('with IGNORE', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1, { + IGNORE: { + maxTimeDiff: 1, + maxValDiff: 1 + } + }), + ['TS.INCRBY', 'key', '1', 'IGNORE', '1', '1'] + ) + }); + + it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => { + assert.deepEqual( + INCRBY.transformArguments('key', 1, { + TIMESTAMP: '*', + RETENTION: 1, + UNCOMPRESSED: true, + CHUNK_SIZE: 1, + LABELS: { label: 'value' }, + IGNORE: { maxTimeDiff: 1, maxValDiff: 1 } + }), + ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*', 'RETENTION', '1', 'UNCOMPRESSED', + 'CHUNK_SIZE', '1', 'LABELS', 'label', 'value', 'IGNORE', '1', '1'] + ); }); + }); - testUtils.testWithClient('client.ts.incrBy', async client => { - assert.equal( - await client.ts.incrBy('key', 1, { - TIMESTAMP: 0 - }), - 0 - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ts.incrBy', async client => { + assert.equal( + typeof await client.ts.incrBy('key', 1), + 'number' + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/INCRBY.ts b/packages/time-series/lib/commands/INCRBY.ts index 1f96801305f..3160d3906d3 100644 --- a/packages/time-series/lib/commands/INCRBY.ts +++ b/packages/time-series/lib/commands/INCRBY.ts @@ -1,10 +1,50 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { IncrDecrOptions, transformIncrDecrArguments } from '.'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { Timestamp, transformTimestampArgument, pushRetentionArgument, pushChunkSizeArgument, Labels, pushLabelsArgument, pushIgnoreArgument } from '.'; +import { TsIgnoreOptions } from './ADD'; -export const FIRST_KEY_INDEX = 1; +export interface TsIncrByOptions { + TIMESTAMP?: Timestamp; + RETENTION?: number; + UNCOMPRESSED?: boolean; + CHUNK_SIZE?: number; + LABELS?: Labels; + IGNORE?: TsIgnoreOptions; +} + +export function transformIncrByArguments( + command: RedisArgument, + key: RedisArgument, + value: number, + options?: TsIncrByOptions +) { + const args = [ + command, + key, + value.toString() + ]; + + if (options?.TIMESTAMP !== undefined && options?.TIMESTAMP !== null) { + args.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP)); + } + + pushRetentionArgument(args, options?.RETENTION); + + if (options?.UNCOMPRESSED) { + args.push('UNCOMPRESSED'); + } + + pushChunkSizeArgument(args, options?.CHUNK_SIZE); + + pushLabelsArgument(args, options?.LABELS); + + pushIgnoreArgument(args, options?.IGNORE); -export function transformArguments(key: string, value: number, options?: IncrDecrOptions): RedisCommandArguments { - return transformIncrDecrArguments('TS.INCRBY', key, value, options); + return args; } -export declare function transformReply(): number; +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments: transformIncrByArguments.bind(undefined, 'TS.INCRBY'), + transformReply: undefined as unknown as () => NumberReply +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/INFO.spec.ts b/packages/time-series/lib/commands/INFO.spec.ts index c02cdd6da4d..e4295b80fa4 100644 --- a/packages/time-series/lib/commands/INFO.spec.ts +++ b/packages/time-series/lib/commands/INFO.spec.ts @@ -1,12 +1,13 @@ -import { strict as assert } from 'assert'; -import { TimeSeriesAggregationType, TimeSeriesDuplicatePolicies } from '.'; +import { strict as assert } from 'node:assert'; +import { TIME_SERIES_DUPLICATE_POLICIES } from '.'; import testUtils, { GLOBAL } from '../test-utils'; -import { InfoReply, transformArguments } from './INFO'; +import INFO, { InfoReply } from './INFO'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; -describe('INFO', () => { +describe('TS.INFO', () => { it('transformArguments', () => { assert.deepEqual( - transformArguments('key'), + INFO.transformArguments('key'), ['TS.INFO', 'key'] ); }); @@ -15,14 +16,14 @@ describe('INFO', () => { await Promise.all([ client.ts.create('key', { LABELS: { id: '1' }, - DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.LAST + DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.LAST }), client.ts.create('key2'), - client.ts.createRule('key', 'key2', TimeSeriesAggregationType.COUNT, 5), + client.ts.createRule('key', 'key2', TIME_SERIES_AGGREGATION_TYPE.COUNT, 5), client.ts.add('key', 1, 10) ]); - assertInfo(await client.ts.info('key')); + assertInfo(await client.ts.info('key') as any); }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/INFO.ts b/packages/time-series/lib/commands/INFO.ts index 25ce3ef54ea..91d4e4bbad3 100644 --- a/packages/time-series/lib/commands/INFO.ts +++ b/packages/time-series/lib/commands/INFO.ts @@ -1,82 +1,128 @@ -import { TimeSeriesAggregationType, TimeSeriesDuplicatePolicies } from '.'; +import { ArrayReply, BlobStringReply, Command, DoubleReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/lib/RESP/types"; +import { TimeSeriesDuplicatePolicies } from "."; +import { TimeSeriesAggregationType } from "./CREATERULE"; +import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; -export const FIRST_KEY_INDEX = 1; +export type InfoRawReplyTypes = SimpleStringReply | + NumberReply | + TimeSeriesDuplicatePolicies | null | + Array<[name: BlobStringReply, value: BlobStringReply]> | + BlobStringReply | + Array<[key: BlobStringReply, timeBucket: NumberReply, aggregationType: TimeSeriesAggregationType]> | + DoubleReply -export const IS_READ_ONLY = true; +export type InfoRawReply = Array; -export function transformArguments(key: string): Array { - return ['TS.INFO', key]; -} - -export type InfoRawReply = [ - 'totalSamples', - number, - 'memoryUsage', - number, - 'firstTimestamp', - number, - 'lastTimestamp', - number, - 'retentionTime', - number, - 'chunkCount', - number, - 'chunkSize', - number, - 'chunkType', - string, - 'duplicatePolicy', - TimeSeriesDuplicatePolicies | null, - 'labels', - Array<[name: string, value: string]>, - 'sourceKey', - string | null, - 'rules', - Array<[key: string, timeBucket: number, aggregationType: TimeSeriesAggregationType]> +export type InfoRawReplyOld = [ + 'totalSamples', + NumberReply, + 'memoryUsage', + NumberReply, + 'firstTimestamp', + NumberReply, + 'lastTimestamp', + NumberReply, + 'retentionTime', + NumberReply, + 'chunkCount', + NumberReply, + 'chunkSize', + NumberReply, + 'chunkType', + SimpleStringReply, + 'duplicatePolicy', + TimeSeriesDuplicatePolicies | null, + 'labels', + ArrayReply<[name: BlobStringReply, value: BlobStringReply]>, + 'sourceKey', + BlobStringReply | null, + 'rules', + ArrayReply<[key: BlobStringReply, timeBucket: NumberReply, aggregationType: TimeSeriesAggregationType]>, + 'ignoreMaxTimeDiff', + NumberReply, + 'ignoreMaxValDiff', + DoubleReply, ]; export interface InfoReply { - totalSamples: number; - memoryUsage: number; - firstTimestamp: number; - lastTimestamp: number; - retentionTime: number; - chunkCount: number; - chunkSize: number; - chunkType: string; - duplicatePolicy: TimeSeriesDuplicatePolicies | null; - labels: Array<{ - name: string; - value: string; - }>; - sourceKey: string | null; - rules: Array<{ - key: string; - timeBucket: number; - aggregationType: TimeSeriesAggregationType - }>; + totalSamples: NumberReply; + memoryUsage: NumberReply; + firstTimestamp: NumberReply; + lastTimestamp: NumberReply; + retentionTime: NumberReply; + chunkCount: NumberReply; + chunkSize: NumberReply; + chunkType: SimpleStringReply; + duplicatePolicy: TimeSeriesDuplicatePolicies | null; + labels: Array<{ + name: BlobStringReply; + value: BlobStringReply; + }>; + sourceKey: BlobStringReply | null; + rules: Array<{ + key: BlobStringReply; + timeBucket: NumberReply; + aggregationType: TimeSeriesAggregationType + }>; + /** Added in 7.4 */ + ignoreMaxTimeDiff: NumberReply; + /** Added in 7.4 */ + ignoreMaxValDiff: DoubleReply; } -export function transformReply(reply: InfoRawReply): InfoReply { - return { - totalSamples: reply[1], - memoryUsage: reply[3], - firstTimestamp: reply[5], - lastTimestamp: reply[7], - retentionTime: reply[9], - chunkCount: reply[11], - chunkSize: reply[13], - chunkType: reply[15], - duplicatePolicy: reply[17], - labels: reply[19].map(([name, value]) => ({ - name, - value - })), - sourceKey: reply[21], - rules: reply[23].map(([key, timeBucket, aggregationType]) => ({ - key, - timeBucket, - aggregationType - })) - }; -} +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments(key: string) { + return ['TS.INFO', key]; + }, + transformReply: { + 2: (reply: InfoRawReply, _, typeMapping?: TypeMapping): InfoReply => { + const ret = {} as any; + + for (let i=0; i < reply.length; i += 2) { + const key = (reply[i] as any).toString(); + + switch (key) { + case 'totalSamples': + case 'memoryUsage': + case 'firstTimestamp': + case 'lastTimestamp': + case 'retentionTime': + case 'chunkCount': + case 'chunkSize': + case 'chunkType': + case 'duplicatePolicy': + case 'sourceKey': + case 'ignoreMaxTimeDiff': + ret[key] = reply[i+1]; + break; + case 'labels': + ret[key] = (reply[i+1] as Array<[name: BlobStringReply, value: BlobStringReply]>).map( + ([name, value]) => ({ + name, + value + }) + ); + break; + case 'rules': + ret[key] = (reply[i+1] as Array<[key: BlobStringReply, timeBucket: NumberReply, aggregationType: TimeSeriesAggregationType]>).map( + ([key, timeBucket, aggregationType]) => ({ + key, + timeBucket, + aggregationType + }) + ); + break; + case 'ignoreMaxValDiff': + ret[key] = transformDoubleReply[2](reply[27] as unknown as BlobStringReply, undefined, typeMapping); + break; + } + } + + return ret; + }, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true + } as const satisfies Command; \ No newline at end of file diff --git a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts index 666689f5194..674f91c60a7 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts @@ -1,30 +1,31 @@ -import { strict as assert } from 'assert'; -import { TimeSeriesAggregationType, TimeSeriesDuplicatePolicies } from '.'; +import { strict as assert } from 'node:assert'; +import { TIME_SERIES_DUPLICATE_POLICIES } from '.'; import testUtils, { GLOBAL } from '../test-utils'; import { assertInfo } from './INFO.spec'; -import { transformArguments } from './INFO_DEBUG'; +import INFO_DEBUG from './INFO_DEBUG'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; -describe('INFO_DEBUG', () => { +describe('TS.INFO_DEBUG', () => { it('transformArguments', () => { assert.deepEqual( - transformArguments('key'), + INFO_DEBUG.transformArguments('key'), ['TS.INFO', 'key', 'DEBUG'] ); }); - testUtils.testWithClient('client.ts.get', async client => { + testUtils.testWithClient('client.ts.infoDebug', async client => { await Promise.all([ client.ts.create('key', { LABELS: { id: '1' }, - DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.LAST + DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.LAST }), client.ts.create('key2'), - client.ts.createRule('key', 'key2', TimeSeriesAggregationType.COUNT, 5), + client.ts.createRule('key', 'key2', TIME_SERIES_AGGREGATION_TYPE.COUNT, 5), client.ts.add('key', 1, 10) ]); const infoDebug = await client.ts.infoDebug('key'); - assertInfo(infoDebug); + assertInfo(infoDebug as any); assert.equal(typeof infoDebug.keySelfName, 'string'); assert.ok(Array.isArray(infoDebug.chunks)); for (const chunk of infoDebug.chunks) { diff --git a/packages/time-series/lib/commands/INFO_DEBUG.ts b/packages/time-series/lib/commands/INFO_DEBUG.ts index 20d6ff5e242..fb2b28b8072 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.ts @@ -1,57 +1,79 @@ -import { - transformArguments as transformInfoArguments, - InfoRawReply, - InfoReply, - transformReply as transformInfoReply -} from './INFO'; +import { BlobStringReply, Command, NumberReply, SimpleStringReply, TypeMapping } from "@redis/client/lib/RESP/types"; +import INFO, { InfoRawReply, InfoRawReplyTypes, InfoReply } from "./INFO"; +import { ReplyUnion } from '@redis/client/lib/RESP/types'; -export { IS_READ_ONLY, FIRST_KEY_INDEX } from './INFO'; - -export function transformArguments(key: string): Array { - const args = transformInfoArguments(key); - args.push('DEBUG'); - return args; -} +type chunkType = Array<[ + 'startTimestamp', + NumberReply, + 'endTimestamp', + NumberReply, + 'samples', + NumberReply, + 'size', + NumberReply, + 'bytesPerSample', + SimpleStringReply +]>; type InfoDebugRawReply = [ - ...InfoRawReply, - 'keySelfName', - string, - 'chunks', - Array<[ - 'startTimestamp', - number, - 'endTimestamp', - number, - 'samples', - number, - 'size', - number, - 'bytesPerSample', - string - ]> + ...InfoRawReply, + 'keySelfName', + BlobStringReply, + 'Chunks', + chunkType ]; -interface InfoDebugReply extends InfoReply { - keySelfName: string; - chunks: Array<{ - startTimestamp: number; - endTimestamp: number; - samples: number; - size: number; - bytesPerSample: string; - }>; -} +export type InfoDebugRawReplyType = InfoRawReplyTypes | chunkType -export function transformReply(rawReply: InfoDebugRawReply): InfoDebugReply { - const reply = transformInfoReply(rawReply as unknown as InfoRawReply); - (reply as InfoDebugReply).keySelfName = rawReply[25]; - (reply as InfoDebugReply).chunks = rawReply[27].map(chunk => ({ - startTimestamp: chunk[1], - endTimestamp: chunk[3], - samples: chunk[5], - size: chunk[7], - bytesPerSample: chunk[9] - })); - return reply as InfoDebugReply; +export interface InfoDebugReply extends InfoReply { + keySelfName: BlobStringReply, + chunks: Array<{ + startTimestamp: NumberReply; + endTimestamp: NumberReply; + samples: NumberReply; + size: NumberReply; + bytesPerSample: SimpleStringReply; + }>; } + +export default { + FIRST_KEY_INDEX: INFO.FIRST_KEY_INDEX, + IS_READ_ONLY: INFO.IS_READ_ONLY, + transformArguments(key: string) { + const args = INFO.transformArguments(key); + args.push('DEBUG'); + return args; + }, + transformReply: { + 2: (reply: InfoDebugRawReply, _, typeMapping?: TypeMapping): InfoDebugReply => { + const ret = INFO.transformReply[2](reply as unknown as InfoRawReply, _, typeMapping) as any; + + for (let i=0; i < reply.length; i += 2) { + const key = (reply[i] as any).toString(); + + switch (key) { + case 'keySelfName': { + ret[key] = reply[i+1]; + break; + } + case 'Chunks': { + ret['chunks'] = (reply[i+1] as chunkType).map( + chunk => ({ + startTimestamp: chunk[1], + endTimestamp: chunk[3], + samples: chunk[5], + size: chunk[7], + bytesPerSample: chunk[9] + }) + ); + break; + } + } + } + + return ret; + }, + 3: undefined as unknown as () => ReplyUnion + }, + unstableResp3: true +} as const satisfies Command; \ No newline at end of file diff --git a/packages/time-series/lib/commands/MADD.spec.ts b/packages/time-series/lib/commands/MADD.spec.ts index eed014f2b14..bbe358e5438 100644 --- a/packages/time-series/lib/commands/MADD.spec.ts +++ b/packages/time-series/lib/commands/MADD.spec.ts @@ -1,39 +1,41 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MADD'; +import MADD from './MADD'; +import { SimpleError } from '@redis/client/lib/errors'; -describe('MADD', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments([{ - key: '1', - timestamp: 0, - value: 0 - }, { - key: '2', - timestamp: 1, - value: 1 - }]), - ['TS.MADD', '1', '0', '0', '2', '1', '1'] - ); - }); +describe('TS.MADD', () => { + it('transformArguments', () => { + assert.deepEqual( + MADD.transformArguments([{ + key: '1', + timestamp: 0, + value: 0 + }, { + key: '2', + timestamp: 1, + value: 1 + }]), + ['TS.MADD', '1', '0', '0', '2', '1', '1'] + ); + }); - // Should we check empty array? + testUtils.testWithClient('client.ts.mAdd', async client => { + const [, reply] = await Promise.all([ + client.ts.create('key'), + client.ts.mAdd([{ + key: 'key', + timestamp: 0, + value: 1 + }, { + key: 'key', + timestamp: 0, + value: 1 + }]) + ]); - testUtils.testWithClient('client.ts.mAdd', async client => { - await client.ts.create('key'); - - assert.deepEqual( - await client.ts.mAdd([{ - key: 'key', - timestamp: 0, - value: 0 - }, { - key: 'key', - timestamp: 1, - value: 1 - }]), - [0, 1] - ); - }, GLOBAL.SERVERS.OPEN); + assert.ok(Array.isArray(reply)); + assert.equal(reply.length, 2); + assert.equal(reply[0], 0); + assert.ok(reply[1] instanceof SimpleError); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/MADD.ts b/packages/time-series/lib/commands/MADD.ts index 426eae7e3f3..59c1ed59bdb 100644 --- a/packages/time-series/lib/commands/MADD.ts +++ b/packages/time-series/lib/commands/MADD.ts @@ -1,25 +1,27 @@ import { Timestamp, transformTimestampArgument } from '.'; +import { ArrayReply, NumberReply, SimpleErrorReply, Command } from '@redis/client/dist/lib/RESP/types'; -export const FIRST_KEY_INDEX = 1; - -interface MAddSample { - key: string; - timestamp: Timestamp; - value: number; +export interface TsMAddSample { + key: string; + timestamp: Timestamp; + value: number; } -export function transformArguments(toAdd: Array): Array { +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: false, + transformArguments(toAdd: Array) { const args = ['TS.MADD']; for (const { key, timestamp, value } of toAdd) { - args.push( - key, - transformTimestampArgument(timestamp), - value.toString() - ); + args.push( + key, + transformTimestampArgument(timestamp), + value.toString() + ); } return args; -} - -export declare function transformReply(): Array; + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MGET.spec.ts b/packages/time-series/lib/commands/MGET.spec.ts index 61da3b96383..b2de0486cfe 100644 --- a/packages/time-series/lib/commands/MGET.spec.ts +++ b/packages/time-series/lib/commands/MGET.spec.ts @@ -1,40 +1,45 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MGET'; +import MGET from './MGET'; -describe('MGET', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('label=value'), - ['TS.MGET', 'FILTER', 'label=value'] - ); - }); +describe('TS.MGET', () => { + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + MGET.transformArguments('label=value'), + ['TS.MGET', 'FILTER', 'label=value'] + ); + }); - it('with LATEST', () => { - assert.deepEqual( - transformArguments('label=value', { - LATEST: true - }), - ['TS.MGET', 'LATEST', 'FILTER', 'label=value'] - ); - }); + it('with LATEST', () => { + assert.deepEqual( + MGET.transformArguments('label=value', { + LATEST: true + }), + ['TS.MGET', 'LATEST', 'FILTER', 'label=value'] + ); }); + }); - testUtils.testWithClient('client.ts.mGet', async client => { - await client.ts.add('key', 0, 0, { - LABELS: { label: 'value' } - }); + testUtils.testWithClient('client.ts.mGet', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mGet('label=value') + ]); - assert.deepEqual( - await client.ts.mGet('label=value'), - [{ - key: 'key', - sample: { - timestamp: 0, - value: 0 - } - }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepStrictEqual(reply, Object.create(null, { + key: { + configurable: true, + enumerable: true, + value: { + sample: { + timestamp: 0, + value: 0 + } + } + } + })); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/MGET.ts b/packages/time-series/lib/commands/MGET.ts index 67315722eb6..2b04b29589b 100644 --- a/packages/time-series/lib/commands/MGET.ts +++ b/packages/time-series/lib/commands/MGET.ts @@ -1,31 +1,61 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { Filter, pushFilterArgument, pushLatestArgument, RawLabels, SampleRawReply, SampleReply, transformSampleReply } from '.'; +import { CommandArguments, Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { resp2MapToValue, resp3MapToValue, SampleRawReply, transformSampleReply } from '.'; -export const IS_READ_ONLY = true; +export interface TsMGetOptions { + LATEST?: boolean; +} + +export function pushLatestArgument(args: CommandArguments, latest?: boolean) { + if (latest) { + args.push('LATEST'); + } -export interface MGetOptions { - LATEST?: boolean; + return args; } -export function transformArguments(filter: Filter, options?: MGetOptions): RedisCommandArguments { - const args = pushLatestArgument(['TS.MGET'], options?.LATEST); - return pushFilterArgument(args, filter); +export function pushFilterArgument(args: CommandArguments, filter: RedisVariadicArgument) { + args.push('FILTER'); + return pushVariadicArguments(args, filter); } -export type MGetRawReply = Array<[ - key: string, - labels: RawLabels, - sample: SampleRawReply -]>; +export type MGetRawReply2 = ArrayReply< + TuplesReply<[ + key: BlobStringReply, + labels: never, + sample: Resp2Reply + ]> +>; -export interface MGetReply { - key: string, - sample: SampleReply -} +export type MGetRawReply3 = MapReply< + BlobStringReply, + TuplesReply<[ + labels: never, + sample: SampleRawReply + ]> +>; -export function transformReply(reply: MGetRawReply): Array { - return reply.map(([key, _, sample]) => ({ - key, - sample: transformSampleReply(sample) - })); -} +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(filter: RedisVariadicArgument, options?: TsMGetOptions) { + const args = pushLatestArgument(['TS.MGET'], options?.LATEST); + return pushFilterArgument(args, filter); + }, + transformReply: { + 2(reply: MGetRawReply2, _, typeMapping?: TypeMapping) { + return resp2MapToValue(reply, ([,, sample]) => { + return { + sample: transformSampleReply[2](sample) + }; + }, typeMapping); + }, + 3(reply: MGetRawReply3) { + return resp3MapToValue(reply, ([, sample]) => { + return { + sample: transformSampleReply[3](sample) + }; + }); + } + } +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts new file mode 100644 index 00000000000..d9820027bb9 --- /dev/null +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts @@ -0,0 +1,46 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MGET_SELECTED_LABELS from './MGET_SELECTED_LABELS'; + +describe('TS.MGET_SELECTED_LABELS', () => { + it('transformArguments', () => { + assert.deepEqual( + MGET_SELECTED_LABELS.transformArguments('label=value', 'label'), + ['TS.MGET', 'SELECTED_LABELS', 'label', 'FILTER', 'label=value'] + ); + }); + + testUtils.testWithClient('client.ts.mGetSelectedLabels', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mGetSelectedLabels('label=value', ['label', 'NX']) + ]); + + assert.deepStrictEqual(reply, Object.create(null, { + key: { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + }, + NX: { + configurable: true, + enumerable: true, + value: null + } + }), + sample: { + timestamp: 0, + value: 0 + } + } + } + })); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts new file mode 100644 index 00000000000..d132972d879 --- /dev/null +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts @@ -0,0 +1,16 @@ +import { Command, BlobStringReply, NullReply } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { TsMGetOptions, pushLatestArgument, pushFilterArgument } from './MGET'; +import { pushSelectedLabelsArguments } from '.'; +import { createTransformMGetLabelsReply } from './MGET_WITHLABELS'; + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(filter: RedisVariadicArgument, selectedLabels: RedisVariadicArgument, options?: TsMGetOptions) { + let args = pushLatestArgument(['TS.MGET'], options?.LATEST); + args = pushSelectedLabelsArguments(args, selectedLabels); + return pushFilterArgument(args, filter); + }, + transformReply: createTransformMGetLabelsReply(), +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts index 55fcfde409d..d3e51d2cab6 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts @@ -1,39 +1,41 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MGET_WITHLABELS'; +import MGET_WITHLABELS from './MGET_WITHLABELS'; -describe('MGET_WITHLABELS', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('label=value'), - ['TS.MGET', 'WITHLABELS', 'FILTER', 'label=value'] - ); - }); +describe('TS.MGET_WITHLABELS', () => { + it('transformArguments', () => { + assert.deepEqual( + MGET_WITHLABELS.transformArguments('label=value'), + ['TS.MGET', 'WITHLABELS', 'FILTER', 'label=value'] + ); + }); - it('with SELECTED_LABELS', () => { - assert.deepEqual( - transformArguments('label=value', { SELECTED_LABELS: 'label' }), - ['TS.MGET', 'SELECTED_LABELS', 'label', 'FILTER', 'label=value'] - ); - }); - }); - - testUtils.testWithClient('client.ts.mGetWithLabels', async client => { - await client.ts.add('key', 0, 0, { - LABELS: { label: 'value' } - }); - - assert.deepEqual( - await client.ts.mGetWithLabels('label=value'), - [{ - key: 'key', - labels: { label: 'value'}, - sample: { - timestamp: 0, - value: 0 - } - }] - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClient('client.ts.mGetWithLabels', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mGetWithLabels('label=value') + ]); + + assert.deepStrictEqual(reply, Object.create(null, { + key: { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + } + }), + sample: { + timestamp: 0, + value: 0 + } + } + } + })); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.ts index 232c17a0ada..679a536f2ab 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.ts @@ -1,37 +1,61 @@ -import { - SelectedLabels, - pushWithLabelsArgument, - Labels, - transformLablesReply, - transformSampleReply, - Filter, - pushFilterArgument -} from '.'; -import { MGetOptions, MGetRawReply, MGetReply } from './MGET'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { TsMGetOptions, pushLatestArgument, pushFilterArgument } from './MGET'; +import { RawLabelValue, resp2MapToValue, resp3MapToValue, SampleRawReply, transformRESP2Labels, transformSampleReply } from '.'; -export const IS_READ_ONLY = true; - -interface MGetWithLabelsOptions extends MGetOptions { - SELECTED_LABELS?: SelectedLabels; +export interface TsMGetWithLabelsOptions extends TsMGetOptions { + SELECTED_LABELS?: RedisVariadicArgument; } -export function transformArguments( - filter: Filter, - options?: MGetWithLabelsOptions -): RedisCommandArguments { - const args = pushWithLabelsArgument(['TS.MGET'], options?.SELECTED_LABELS); - return pushFilterArgument(args, filter); -} +export type MGetLabelsRawReply2 = ArrayReply< + TuplesReply<[ + key: BlobStringReply, + labels: ArrayReply< + TuplesReply<[ + label: BlobStringReply, + value: T + ]> + >, + sample: Resp2Reply + ]> +>; -export interface MGetWithLabelsReply extends MGetReply { - labels: Labels; -}; +export type MGetLabelsRawReply3 = MapReply< + BlobStringReply, + TuplesReply<[ + labels: MapReply, + sample: SampleRawReply + ]> +>; -export function transformReply(reply: MGetRawReply): Array { - return reply.map(([key, labels, sample]) => ({ - key, - labels: transformLablesReply(labels), - sample: transformSampleReply(sample) - })); +export function createTransformMGetLabelsReply() { + return { + 2(reply: MGetLabelsRawReply2, _, typeMapping?: TypeMapping) { + return resp2MapToValue(reply, ([, labels, sample]) => { + return { + labels: transformRESP2Labels(labels), + sample: transformSampleReply[2](sample) + }; + }, typeMapping); + }, + 3(reply: MGetLabelsRawReply3) { + return resp3MapToValue(reply, ([labels, sample]) => { + return { + labels, + sample: transformSampleReply[3](sample) + }; + }); + } + } satisfies Command['transformReply']; } + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(filter: RedisVariadicArgument, options?: TsMGetOptions) { + const args = pushLatestArgument(['TS.MGET'], options?.LATEST); + args.push('WITHLABELS'); + return pushFilterArgument(args, filter); + }, + transformReply: createTransformMGetLabelsReply(), +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MRANGE.spec.ts b/packages/time-series/lib/commands/MRANGE.spec.ts index 4228cc06fb7..9d41763eb02 100644 --- a/packages/time-series/lib/commands/MRANGE.spec.ts +++ b/packages/time-series/lib/commands/MRANGE.spec.ts @@ -1,50 +1,62 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MRANGE'; -import { TimeSeriesAggregationType, TimeSeriesReducers } from '.'; +import MRANGE from './MRANGE'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; -describe('MRANGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('-', '+', 'label=value', { - FILTER_BY_TS: [0], - FILTER_BY_VALUE: { - min: 0, - max: 1 - }, - COUNT: 1, - ALIGN: '-', - AGGREGATION: { - type: TimeSeriesAggregationType.AVERAGE, - timeBucket: 1 - }, - GROUPBY: { - label: 'label', - reducer: TimeSeriesReducers.SUM - }, - }), - ['TS.MRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1', - 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', 'FILTER', 'label=value', - 'GROUPBY', 'label', 'REDUCE', 'SUM'] - ); - }); +describe('TS.MRANGE', () => { + it('transformArguments', () => { + assert.deepEqual( + MRANGE.transformArguments('-', '+', 'label=value', { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', + 'AGGREGATION', 'AVG', '1', + 'FILTER', 'label=value' + ] + ); + }); - testUtils.testWithClient('client.ts.mRange', async client => { - await client.ts.add('key', 0, 0, { - LABELS: { label: 'value'} - }); + testUtils.testWithClient('client.ts.mRange', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { + label: 'value' + } + }), + client.ts.mRange('-', '+', 'label=value', { + COUNT: 1 + }) + ]); - assert.deepEqual( - await client.ts.mRange('-', '+', 'label=value', { - COUNT: 1 - }), - [{ - key: 'key', - samples: [{ - timestamp: 0, - value: 0 - }] - }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepStrictEqual( + reply, + Object.create(null, { + key: { + configurable: true, + enumerable: true, + value: [{ + timestamp: 0, + value: 0 + }] + } + }) + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/MRANGE.ts b/packages/time-series/lib/commands/MRANGE.ts index d589ac0332a..bbc93a70dad 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -1,21 +1,58 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { MRangeOptions, Timestamp, pushMRangeArguments, Filter } from '.'; +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; +import { TsRangeOptions, pushRangeArguments } from './RANGE'; +import { pushFilterArgument } from './MGET'; -export const IS_READ_ONLY = true; +export type TsMRangeRawReply2 = ArrayReply< + TuplesReply<[ + key: BlobStringReply, + labels: never, // empty array without WITHLABELS or SELECTED_LABELS + samples: ArrayReply> + ]> +>; -export function transformArguments( +export type TsMRangeRawReply3 = MapReply< + BlobStringReply, + TuplesReply<[ + labels: never, // empty hash without WITHLABELS or SELECTED_LABELS + metadata: never, // ?! + samples: ArrayReply + ]> +>; + +export function createTransformMRangeArguments(command: RedisArgument) { + return ( fromTimestamp: Timestamp, toTimestamp: Timestamp, - filters: Filter, - options?: MRangeOptions -): RedisCommandArguments { - return pushMRangeArguments( - ['TS.MRANGE'], - fromTimestamp, - toTimestamp, - filters, - options + filter: RedisVariadicArgument, + options?: TsRangeOptions + ) => { + const args = pushRangeArguments( + [command], + fromTimestamp, + toTimestamp, + options ); + + return pushFilterArgument(args, filter); + }; } -export { transformMRangeReply as transformReply } from '.'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments: createTransformMRangeArguments('TS.MRANGE'), + transformReply: { + 2(reply: TsMRangeRawReply2, _?: any, typeMapping?: TypeMapping) { + return resp2MapToValue(reply, ([_key, _labels, samples]) => { + return transformSamplesReply[2](samples); + }, typeMapping); + }, + 3(reply: TsMRangeRawReply3) { + return resp3MapToValue(reply, ([_labels, _metadata, samples]) => { + return transformSamplesReply[3](samples); + }); + } + }, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts b/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts new file mode 100644 index 00000000000..c0d05425ff4 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts @@ -0,0 +1,66 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MRANGE_GROUPBY, { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; + +describe('TS.MRANGE_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + MRANGE_GROUPBY.transformArguments('-', '+', 'label=value', { + REDUCE: TIME_SERIES_REDUCERS.AVG, + label: 'label' + }, { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', + 'FILTER', 'label=value', + 'GROUPBY', 'label', 'REDUCE', 'AVG' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRangeGroupBy', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRangeGroupBy('-', '+', 'label=value', { + REDUCE: TIME_SERIES_REDUCERS.AVG, + label: 'label' + }) + ]); + + assert.deepStrictEqual( + reply, + Object.create(null, { + 'label=value': { + configurable: true, + enumerable: true, + value: { + samples: [{ + timestamp: 0, + value: 0 + }] + } + } + }) + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts new file mode 100644 index 00000000000..3b4e94eac20 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts @@ -0,0 +1,108 @@ +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument, TuplesToMapReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; +import { TsRangeOptions, pushRangeArguments } from './RANGE'; +import { pushFilterArgument } from './MGET'; + +export const TIME_SERIES_REDUCERS = { + AVG: 'AVG', + SUM: 'SUM', + MIN: 'MIN', + MAX: 'MAX', + RANGE: 'RANGE', + COUNT: 'COUNT', + STD_P: 'STD.P', + STD_S: 'STD.S', + VAR_P: 'VAR.P', + VAR_S: 'VAR.S' +} as const; + +export type TimeSeriesReducer = typeof TIME_SERIES_REDUCERS[keyof typeof TIME_SERIES_REDUCERS]; + +export interface TsMRangeGroupBy { + label: RedisArgument; + REDUCE: TimeSeriesReducer; +} + +export function pushGroupByArguments(args: Array, groupBy: TsMRangeGroupBy) { + args.push('GROUPBY', groupBy.label, 'REDUCE', groupBy.REDUCE); +} + +export type TsMRangeGroupByRawReply2 = ArrayReply< + TuplesReply<[ + key: BlobStringReply, + labels: never, // empty array without WITHLABELS or SELECTED_LABELS + samples: ArrayReply> + ]> +>; + +export type TsMRangeGroupByRawMetadataReply3 = TuplesToMapReply<[ + [BlobStringReply<'sources'>, ArrayReply] +]>; + +export type TsMRangeGroupByRawReply3 = MapReply< + BlobStringReply, + TuplesReply<[ + labels: never, // empty hash without WITHLABELS or SELECTED_LABELS + metadata1: never, // ?! + metadata2: TsMRangeGroupByRawMetadataReply3, + samples: ArrayReply + ]> +>; + +export function createTransformMRangeGroupByArguments(command: RedisArgument) { + return ( + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + filter: RedisVariadicArgument, + groupBy: TsMRangeGroupBy, + options?: TsRangeOptions + ) => { + let args = pushRangeArguments( + [command], + fromTimestamp, + toTimestamp, + options + ); + + args = pushFilterArgument(args, filter); + + pushGroupByArguments(args, groupBy); + + return args; + }; +} + +export function extractResp3MRangeSources(raw: TsMRangeGroupByRawMetadataReply3) { + const unwrappedMetadata2 = raw as unknown as UnwrapReply; + if (unwrappedMetadata2 instanceof Map) { + return unwrappedMetadata2.get('sources')!; + } else if (unwrappedMetadata2 instanceof Array) { + return unwrappedMetadata2[1]; + } else { + return unwrappedMetadata2.sources; + } +} + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments: createTransformMRangeGroupByArguments('TS.MRANGE'), + transformReply: { + 2(reply: TsMRangeGroupByRawReply2, _?: any, typeMapping?: TypeMapping) { + return resp2MapToValue(reply, ([_key, _labels, samples]) => { + return { + samples: transformSamplesReply[2](samples) + }; + }, typeMapping); + }, + 3(reply: TsMRangeGroupByRawReply3) { + return resp3MapToValue(reply, ([_labels, _metadata1, metadata2, samples]) => { + return { + sources: extractResp3MRangeSources(metadata2), + samples: transformSamplesReply[3](samples) + }; + }); + } + }, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts new file mode 100644 index 00000000000..5c15bad89e8 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts @@ -0,0 +1,72 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; + +describe('TS.MRANGE_SELECTED_LABELS', () => { + it('transformArguments', () => { + assert.deepEqual( + MRANGE_SELECTED_LABELS.transformArguments('-', '+', 'label', 'label=value', { + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MRANGE', '-', '+', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', + 'SELECTED_LABELS', 'label', + 'FILTER', 'label=value' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRangeSelectedLabels', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRangeSelectedLabels('-', '+', ['label', 'NX'], 'label=value', { + COUNT: 1 + }) + ]); + + assert.deepStrictEqual( + reply, + Object.create(null, { + key: { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + }, + NX: { + configurable: true, + enumerable: true, + value: null + } + }), + samples: [{ + timestamp: 0, + value: 0 + }] + } + } + }) + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts new file mode 100644 index 00000000000..f91f9583330 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts @@ -0,0 +1,70 @@ +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, NullReply, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { pushSelectedLabelsArguments, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2Labels, transformSamplesReply } from '.'; +import { TsRangeOptions, pushRangeArguments } from './RANGE'; +import { pushFilterArgument } from './MGET'; + +export type TsMRangeSelectedLabelsRawReply2 = ArrayReply< + TuplesReply<[ + key: BlobStringReply, + labels: ArrayReply>, + samples: ArrayReply> + ]> +>; + +export type TsMRangeSelectedLabelsRawReply3 = MapReply< + BlobStringReply, + TuplesReply<[ + labels: MapReply, + metadata: never, // ?! + samples: ArrayReply + ]> +>; + +export function createTransformMRangeSelectedLabelsArguments(command: RedisArgument) { + return ( + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + selectedLabels: RedisVariadicArgument, + filter: RedisVariadicArgument, + options?: TsRangeOptions + ) => { + let args = pushRangeArguments( + [command], + fromTimestamp, + toTimestamp, + options + ); + + args = pushSelectedLabelsArguments(args, selectedLabels); + + return pushFilterArgument(args, filter); + }; +} + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments: createTransformMRangeSelectedLabelsArguments('TS.MRANGE'), + transformReply: { + 2(reply: TsMRangeSelectedLabelsRawReply2, _?: any, typeMapping?: TypeMapping) { + return resp2MapToValue(reply, ([_key, labels, samples]) => { + return { + labels: transformRESP2Labels(labels, typeMapping), + samples: transformSamplesReply[2](samples) + }; + }, typeMapping); + }, + 3(reply: TsMRangeSelectedLabelsRawReply3) { + return resp3MapToValue(reply, ([_key, labels, samples]) => { + return { + labels, + samples: transformSamplesReply[3](samples) + }; + }); + } + }, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts new file mode 100644 index 00000000000..90090a851aa --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts @@ -0,0 +1,80 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MRANGE_SELECTED_LABELS_GROUPBY from './MRANGE_SELECTED_LABELS_GROUPBY'; +import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; + +describe('TS.MRANGE_SELECTED_LABELS_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + MRANGE_SELECTED_LABELS_GROUPBY.transformArguments('-', '+', 'label', 'label=value', { + REDUCE: TIME_SERIES_REDUCERS.AVG, + label: 'label' + }, { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', + 'SELECTED_LABELS', 'label', + 'FILTER', 'label=value', + 'GROUPBY', 'label', 'REDUCE', 'AVG' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRangeSelectedLabelsGroupBy', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRangeSelectedLabelsGroupBy('-', '+', ['label', 'NX'], 'label=value', { + REDUCE: TIME_SERIES_REDUCERS.AVG, + label: 'label' + }) + ]); + + assert.deepStrictEqual( + reply, + Object.create(null, { + 'label=value': { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + }, + NX: { + configurable: true, + enumerable: true, + value: null + } + }), + samples: [{ + timestamp: 0, + value: 0 + }] + } + } + }) + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts new file mode 100644 index 00000000000..7a798c41137 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts @@ -0,0 +1,63 @@ +import { Command, ArrayReply, BlobStringReply, MapReply, TuplesReply, RedisArgument, NullReply } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { pushSelectedLabelsArguments, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; +import { TsRangeOptions, pushRangeArguments } from './RANGE'; +import { extractResp3MRangeSources, pushGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; +import { pushFilterArgument } from './MGET'; +import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS'; + +export type TsMRangeWithLabelsGroupByRawReply3 = MapReply< + BlobStringReply, + TuplesReply<[ + labels: MapReply, + metadata: never, // ?! + metadata2: TsMRangeGroupByRawMetadataReply3, + samples: ArrayReply + ]> +>; + +export function createMRangeSelectedLabelsGroupByTransformArguments( + command: RedisArgument +) { + return ( + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + selectedLabels: RedisVariadicArgument, + filter: RedisVariadicArgument, + groupBy: TsMRangeGroupBy, + options?: TsRangeOptions + ) => { + let args = pushRangeArguments( + [command], + fromTimestamp, + toTimestamp, + options + ); + + args = pushSelectedLabelsArguments(args, selectedLabels); + + args = pushFilterArgument(args, filter); + + pushGroupByArguments(args, groupBy); + + return args; + }; +} + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments: createMRangeSelectedLabelsGroupByTransformArguments('TS.MRANGE'), + transformReply: { + 2: MRANGE_SELECTED_LABELS.transformReply[2], + 3(reply: TsMRangeWithLabelsGroupByRawReply3) { + return resp3MapToValue(reply, ([labels, _metadata, metadata2, samples]) => { + return { + labels, + sources: extractResp3MRangeSources(metadata2), + samples: transformSamplesReply[3](samples) + }; + }); + } + }, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts index 983114f840e..fabf04b60dc 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts @@ -1,52 +1,68 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MRANGE_WITHLABELS'; -import { TimeSeriesAggregationType, TimeSeriesReducers } from '.'; +import MRANGE_WITHLABELS from './MRANGE_WITHLABELS'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; -describe('MRANGE_WITHLABELS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('-', '+', 'label=value', { - FILTER_BY_TS: [0], - FILTER_BY_VALUE: { - min: 0, - max: 1 - }, - SELECTED_LABELS: ['label'], - COUNT: 1, - ALIGN: '-', - AGGREGATION: { - type: TimeSeriesAggregationType.AVERAGE, - timeBucket: 1 - }, - GROUPBY: { - label: 'label', - reducer: TimeSeriesReducers.SUM - }, - }), - ['TS.MRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1', - 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', 'SELECTED_LABELS', 'label', - 'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'SUM'] - ); - }); +describe('TS.MRANGE_WITHLABELS', () => { + it('transformArguments', () => { + assert.deepEqual( + MRANGE_WITHLABELS.transformArguments('-', '+', 'label=value', { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', + 'AGGREGATION', 'AVG', '1', + 'WITHLABELS', + 'FILTER', 'label=value' + ] + ); + }); - testUtils.testWithClient('client.ts.mRangeWithLabels', async client => { - await client.ts.add('key', 0, 0, { - LABELS: { label: 'value'} - }); + testUtils.testWithClient('client.ts.mRangeWithLabels', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRangeWithLabels('-', '+', 'label=value') + ]); - assert.deepEqual( - await client.ts.mRangeWithLabels('-', '+', 'label=value', { - COUNT: 1 + assert.deepStrictEqual( + reply, + Object.create(null, { + key: { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + } }), - [{ - key: 'key', - labels: { label: 'value' }, - samples: [{ - timestamp: 0, - value: 0 - }] + samples: [{ + timestamp: 0, + value: 0 }] - ); - }, GLOBAL.SERVERS.OPEN); + } + } + }) + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts index 16b7920e82c..ab7a4ec8f6a 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts @@ -1,21 +1,78 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { Timestamp, MRangeWithLabelsOptions, pushMRangeWithLabelsArguments } from '.'; +import { Command, UnwrapReply, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; +import { TsRangeOptions, pushRangeArguments } from './RANGE'; +import { pushFilterArgument } from './MGET'; -export const IS_READ_ONLY = true; +export type TsMRangeWithLabelsRawReply2 = ArrayReply< + TuplesReply<[ + key: BlobStringReply, + labels: ArrayReply>, + samples: ArrayReply> + ]> +>; -export function transformArguments( +export type TsMRangeWithLabelsRawReply3 = MapReply< + BlobStringReply, + TuplesReply<[ + labels: MapReply, + metadata: never, // ?! + samples: ArrayReply + ]> +>; + +export function createTransformMRangeWithLabelsArguments(command: RedisArgument) { + return ( fromTimestamp: Timestamp, toTimestamp: Timestamp, - filters: string | Array, - options?: MRangeWithLabelsOptions -): RedisCommandArguments { - return pushMRangeWithLabelsArguments( - ['TS.MRANGE'], - fromTimestamp, - toTimestamp, - filters, - options + filter: RedisVariadicArgument, + options?: TsRangeOptions + ) => { + const args = pushRangeArguments( + [command], + fromTimestamp, + toTimestamp, + options ); + + args.push('WITHLABELS'); + + return pushFilterArgument(args, filter); + }; } -export { transformMRangeWithLabelsReply as transformReply } from '.'; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments: createTransformMRangeWithLabelsArguments('TS.MRANGE'), + transformReply: { + 2(reply: TsMRangeWithLabelsRawReply2, _?: any, typeMapping?: TypeMapping) { + return resp2MapToValue(reply, ([_key, labels, samples]) => { + const unwrappedLabels = labels as unknown as UnwrapReply; + // TODO: use Map type mapping for labels + const labelsObject: Record = Object.create(null); + for (const tuple of unwrappedLabels) { + const [key, value] = tuple as unknown as UnwrapReply; + const unwrappedKey = key as unknown as UnwrapReply; + labelsObject[unwrappedKey.toString()] = value; + } + + return { + labels: labelsObject, + samples: transformSamplesReply[2](samples) + }; + }, typeMapping); + }, + 3(reply: TsMRangeWithLabelsRawReply3) { + return resp3MapToValue(reply, ([labels, _metadata, samples]) => { + return { + labels, + samples: transformSamplesReply[3](samples) + }; + }); + } + }, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts new file mode 100644 index 00000000000..755c3aca320 --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts @@ -0,0 +1,77 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MRANGE_WITHLABELS_GROUPBY from './MRANGE_WITHLABELS_GROUPBY'; +import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; + +describe('TS.MRANGE_WITHLABELS_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + MRANGE_WITHLABELS_GROUPBY.transformArguments('-', '+', 'label=value', { + label: 'label', + REDUCE: TIME_SERIES_REDUCERS.AVG + }, { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', + 'AGGREGATION', 'AVG', '1', + 'WITHLABELS', + 'FILTER', 'label=value', + 'GROUPBY', 'label', 'REDUCE', 'AVG' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRangeWithLabelsGroupBy', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRangeWithLabelsGroupBy('-', '+', 'label=value', { + label: 'label', + REDUCE: TIME_SERIES_REDUCERS.AVG + }) + ]); + + assert.deepStrictEqual( + reply, + Object.create(null, { + 'label=value': { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + } + }), + sources: ['key'], + samples: [{ + timestamp: 0, + value: 0 + }] + } + } + }) + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts new file mode 100644 index 00000000000..7c5e0af368b --- /dev/null +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts @@ -0,0 +1,79 @@ +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2LabelsWithSources, transformSamplesReply } from '.'; +import { TsRangeOptions, pushRangeArguments } from './RANGE'; +import { extractResp3MRangeSources, pushGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; +import { pushFilterArgument } from './MGET'; + +export type TsMRangeWithLabelsGroupByRawReply2 = ArrayReply< + TuplesReply<[ + key: BlobStringReply, + labels: ArrayReply>, + samples: ArrayReply> + ]> +>; + +export type TsMRangeWithLabelsGroupByRawReply3 = MapReply< + BlobStringReply, + TuplesReply<[ + labels: MapReply, + metadata: never, // ?! + metadata2: TsMRangeGroupByRawMetadataReply3, + samples: ArrayReply + ]> +>; + +export function createMRangeWithLabelsGroupByTransformArguments(command: RedisArgument) { + return ( + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + filter: RedisVariadicArgument, + groupBy: TsMRangeGroupBy, + options?: TsRangeOptions + ) => { + let args = pushRangeArguments( + [command], + fromTimestamp, + toTimestamp, + options + ); + + args.push('WITHLABELS'); + + args = pushFilterArgument(args, filter); + + pushGroupByArguments(args, groupBy); + + return args; + }; +} + +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments: createMRangeWithLabelsGroupByTransformArguments('TS.MRANGE'), + transformReply: { + 2(reply: TsMRangeWithLabelsGroupByRawReply2, _?: any, typeMapping?: TypeMapping) { + return resp2MapToValue(reply, ([_key, labels, samples]) => { + const transformed = transformRESP2LabelsWithSources(labels); + return { + labels: transformed.labels, + sources: transformed.sources, + samples: transformSamplesReply[2](samples) + }; + }, typeMapping); + }, + 3(reply: TsMRangeWithLabelsGroupByRawReply3) { + return resp3MapToValue(reply, ([labels, _metadata, metadata2, samples]) => { + return { + labels, + sources: extractResp3MRangeSources(metadata2), + samples: transformSamplesReply[3](samples) + }; + }); + } + }, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE.spec.ts b/packages/time-series/lib/commands/MREVRANGE.spec.ts index 6e5825d36d6..8d6b8d3c148 100644 --- a/packages/time-series/lib/commands/MREVRANGE.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE.spec.ts @@ -1,50 +1,62 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MREVRANGE'; -import { TimeSeriesAggregationType, TimeSeriesReducers } from '.'; +import MREVRANGE from './MREVRANGE'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; -describe('MREVRANGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('-', '+', 'label=value', { - FILTER_BY_TS: [0], - FILTER_BY_VALUE: { - min: 0, - max: 1 - }, - COUNT: 1, - ALIGN: '-', - AGGREGATION: { - type: TimeSeriesAggregationType.AVERAGE, - timeBucket: 1 - }, - GROUPBY: { - label: 'label', - reducer: TimeSeriesReducers.SUM - }, - }), - ['TS.MREVRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1', - 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', 'FILTER', 'label=value', - 'GROUPBY', 'label', 'REDUCE', 'SUM'] - ); - }); +describe('TS.MREVRANGE', () => { + it('transformArguments', () => { + assert.deepEqual( + MREVRANGE.transformArguments('-', '+', 'label=value', { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MREVRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', + 'AGGREGATION', 'AVG', '1', + 'FILTER', 'label=value' + ] + ); + }); - testUtils.testWithClient('client.ts.mRevRange', async client => { - await client.ts.add('key', 0, 0, { - LABELS: { label: 'value'} - }); + testUtils.testWithClient('client.ts.mRevRange', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { + label: 'value' + } + }), + client.ts.mRevRange('-', '+', 'label=value', { + COUNT: 1 + }) + ]); - assert.deepEqual( - await client.ts.mRevRange('-', '+', 'label=value', { - COUNT: 1 - }), - [{ - key: 'key', - samples: [{ - timestamp: 0, - value: 0 - }] - }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepStrictEqual( + reply, + Object.create(null, { + key: { + configurable: true, + enumerable: true, + value: [{ + timestamp: 0, + value: 0 + }] + } + }) + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/MREVRANGE.ts b/packages/time-series/lib/commands/MREVRANGE.ts index 127c052ffe0..097176e6832 100644 --- a/packages/time-series/lib/commands/MREVRANGE.ts +++ b/packages/time-series/lib/commands/MREVRANGE.ts @@ -1,21 +1,9 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { MRangeOptions, Timestamp, pushMRangeArguments, Filter } from '.'; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE, { createTransformMRangeArguments } from './MRANGE'; -export const IS_READ_ONLY = true; - -export function transformArguments( - fromTimestamp: Timestamp, - toTimestamp: Timestamp, - filters: Filter, - options?: MRangeOptions -): RedisCommandArguments { - return pushMRangeArguments( - ['TS.MREVRANGE'], - fromTimestamp, - toTimestamp, - filters, - options - ); -} - -export { transformMRangeReply as transformReply } from '.'; +export default { + FIRST_KEY_INDEX: MRANGE.FIRST_KEY_INDEX, + IS_READ_ONLY: MRANGE.IS_READ_ONLY, + transformArguments: createTransformMRangeArguments('TS.MREVRANGE'), + transformReply: MRANGE.transformReply, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts new file mode 100644 index 00000000000..9ccebc6c517 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts @@ -0,0 +1,67 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MREVRANGE_GROUPBY from './MREVRANGE_GROUPBY'; +import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; + +describe('TS.MREVRANGE_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + MREVRANGE_GROUPBY.transformArguments('-', '+', 'label=value', { + REDUCE: TIME_SERIES_REDUCERS.AVG, + label: 'label' + }, { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MREVRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', + 'FILTER', 'label=value', + 'GROUPBY', 'label', 'REDUCE', 'AVG' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRevRangeGroupBy', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRevRangeGroupBy('-', '+', 'label=value', { + REDUCE: TIME_SERIES_REDUCERS.AVG, + label: 'label' + }) + ]); + + assert.deepStrictEqual( + reply, + Object.create(null, { + 'label=value': { + configurable: true, + enumerable: true, + value: { + samples: [{ + timestamp: 0, + value: 0 + }] + } + } + }) + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts new file mode 100644 index 00000000000..24b2e6142f6 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts @@ -0,0 +1,9 @@ +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE_GROUPBY, { createTransformMRangeGroupByArguments } from './MRANGE_GROUPBY'; + +export default { + FIRST_KEY_INDEX: MRANGE_GROUPBY.FIRST_KEY_INDEX, + IS_READ_ONLY: MRANGE_GROUPBY.IS_READ_ONLY, + transformArguments: createTransformMRangeGroupByArguments('TS.MREVRANGE'), + transformReply: MRANGE_GROUPBY.transformReply, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts new file mode 100644 index 00000000000..f0533010b84 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts @@ -0,0 +1,73 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MREVRANGE_SELECTED_LABELS from './MREVRANGE_SELECTED_LABELS'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; + +describe('TS.MREVRANGE_SELECTED_LABELS', () => { + it('transformArguments', () => { + assert.deepEqual( + MREVRANGE_SELECTED_LABELS.transformArguments('-', '+', 'label', 'label=value', { + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MREVRANGE', '-', '+', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', + 'SELECTED_LABELS', 'label', + 'FILTER', 'label=value' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRevRangeSelectedLabels', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRevRangeSelectedLabels('-', '+', ['label', 'NX'], 'label=value', { + COUNT: 1 + }) + ]); + + assert.deepStrictEqual( + reply, + Object.create(null, { + key: { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + }, + NX: { + configurable: true, + enumerable: true, + value: null + } + }), + samples: [{ + timestamp: 0, + value: 0 + }] + } + + } + }) + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts new file mode 100644 index 00000000000..8656b768c28 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts @@ -0,0 +1,9 @@ +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE_SELECTED_LABELS, { createTransformMRangeSelectedLabelsArguments } from './MRANGE_SELECTED_LABELS'; + +export default { + FIRST_KEY_INDEX: MRANGE_SELECTED_LABELS.FIRST_KEY_INDEX, + IS_READ_ONLY: MRANGE_SELECTED_LABELS.IS_READ_ONLY, + transformArguments: createTransformMRangeSelectedLabelsArguments('TS.MREVRANGE'), + transformReply: MRANGE_SELECTED_LABELS.transformReply, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts new file mode 100644 index 00000000000..34ef4ff79a0 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts @@ -0,0 +1,80 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MREVRANGE_SELECTED_LABELS_GROUPBY from './MREVRANGE_SELECTED_LABELS_GROUPBY'; +import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; + +describe('TS.MREVRANGE_SELECTED_LABELS_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + MREVRANGE_SELECTED_LABELS_GROUPBY.transformArguments('-', '+', 'label', 'label=value', { + REDUCE: TIME_SERIES_REDUCERS.AVG, + label: 'label' + }, { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MREVRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', + 'SELECTED_LABELS', 'label', + 'FILTER', 'label=value', + 'GROUPBY', 'label', 'REDUCE', 'AVG' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRevRangeSelectedLabelsGroupBy', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRevRangeSelectedLabelsGroupBy('-', '+', ['label', 'NX'], 'label=value', { + REDUCE: TIME_SERIES_REDUCERS.AVG, + label: 'label' + }) + ]); + + assert.deepStrictEqual( + reply, + Object.create(null, { + 'label=value': { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + }, + NX: { + configurable: true, + enumerable: true, + value: null + } + }), + samples: [{ + timestamp: 0, + value: 0 + }] + } + } + }) + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts new file mode 100644 index 00000000000..f47330367b7 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts @@ -0,0 +1,9 @@ +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE_SELECTED_LABELS_GROUPBY, { createMRangeSelectedLabelsGroupByTransformArguments } from './MRANGE_SELECTED_LABELS_GROUPBY'; + +export default { + FIRST_KEY_INDEX: MRANGE_SELECTED_LABELS_GROUPBY.FIRST_KEY_INDEX, + IS_READ_ONLY: MRANGE_SELECTED_LABELS_GROUPBY.IS_READ_ONLY, + transformArguments: createMRangeSelectedLabelsGroupByTransformArguments('TS.MREVRANGE'), + transformReply: MRANGE_SELECTED_LABELS_GROUPBY.transformReply, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts index 7e80e965d4e..eb88f233e43 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts @@ -1,52 +1,68 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './MREVRANGE_WITHLABELS'; -import { TimeSeriesAggregationType, TimeSeriesReducers } from '.'; +import MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; -describe('MREVRANGE_WITHLABELS', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('-', '+', 'label=value', { - FILTER_BY_TS: [0], - FILTER_BY_VALUE: { - min: 0, - max: 1 - }, - SELECTED_LABELS: ['label'], - COUNT: 1, - ALIGN: '-', - AGGREGATION: { - type: TimeSeriesAggregationType.AVERAGE, - timeBucket: 1 - }, - GROUPBY: { - label: 'label', - reducer: TimeSeriesReducers.SUM - }, - }), - ['TS.MREVRANGE', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', '0', '1', - 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1', 'SELECTED_LABELS', 'label', - 'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'SUM'] - ); - }); +describe('TS.MREVRANGE_WITHLABELS', () => { + it('transformArguments', () => { + assert.deepEqual( + MREVRANGE_WITHLABELS.transformArguments('-', '+', 'label=value', { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MREVRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', + 'AGGREGATION', 'AVG', '1', + 'WITHLABELS', + 'FILTER', 'label=value' + ] + ); + }); - testUtils.testWithClient('client.ts.mRevRangeWithLabels', async client => { - await client.ts.add('key', 0, 0, { - LABELS: { label: 'value'} - }); + testUtils.testWithClient('client.ts.mRevRangeWithLabels', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRevRangeWithLabels('-', '+', 'label=value') + ]); - assert.deepEqual( - await client.ts.mRevRangeWithLabels('-', '+', 'label=value', { - COUNT: 1 + assert.deepStrictEqual( + reply, + Object.create(null, { + key: { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + } }), - [{ - key: 'key', - labels: { label: 'value' }, - samples: [{ - timestamp: 0, - value: 0 - }] + samples: [{ + timestamp: 0, + value: 0 }] - ); - }, GLOBAL.SERVERS.OPEN); + } + } + }) + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts index 21a0ebc69c3..81356d845fd 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts @@ -1,21 +1,9 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { Timestamp, MRangeWithLabelsOptions, pushMRangeWithLabelsArguments, Filter } from '.'; +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE_WITHLABELS, { createTransformMRangeWithLabelsArguments } from './MRANGE_WITHLABELS'; -export const IS_READ_ONLY = true; - -export function transformArguments( - fromTimestamp: Timestamp, - toTimestamp: Timestamp, - filters: Filter, - options?: MRangeWithLabelsOptions -): RedisCommandArguments { - return pushMRangeWithLabelsArguments( - ['TS.MREVRANGE'], - fromTimestamp, - toTimestamp, - filters, - options - ); -} - -export { transformMRangeWithLabelsReply as transformReply } from '.'; +export default { + FIRST_KEY_INDEX: MRANGE_WITHLABELS.FIRST_KEY_INDEX, + IS_READ_ONLY: MRANGE_WITHLABELS.IS_READ_ONLY, + transformArguments: createTransformMRangeWithLabelsArguments('TS.MREVRANGE'), + transformReply: MRANGE_WITHLABELS.transformReply, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts new file mode 100644 index 00000000000..da2c358b330 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts @@ -0,0 +1,77 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import MREVRANGE_WITHLABELS_GROUPBY from './MREVRANGE_WITHLABELS_GROUPBY'; +import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; + +describe('TS.MREVRANGE_WITHLABELS_GROUPBY', () => { + it('transformArguments', () => { + assert.deepEqual( + MREVRANGE_WITHLABELS_GROUPBY.transformArguments('-', '+', 'label=value', { + label: 'label', + REDUCE: TIME_SERIES_REDUCERS.AVG + }, { + LATEST: true, + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 0, + max: 1 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.MREVRANGE', '-', '+', + 'LATEST', + 'FILTER_BY_TS', '0', + 'FILTER_BY_VALUE', '0', '1', + 'COUNT', '1', + 'ALIGN', '-', + 'AGGREGATION', 'AVG', '1', + 'WITHLABELS', + 'FILTER', 'label=value', + 'GROUPBY', 'label', 'REDUCE', 'AVG' + ] + ); + }); + + testUtils.testWithClient('client.ts.mRevRangeWithLabelsGroupBy', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 0, 0, { + LABELS: { label: 'value' } + }), + client.ts.mRevRangeWithLabelsGroupBy('-', '+', 'label=value', { + label: 'label', + REDUCE: TIME_SERIES_REDUCERS.AVG + }) + ]); + + assert.deepStrictEqual( + reply, + Object.create(null, { + 'label=value': { + configurable: true, + enumerable: true, + value: { + labels: Object.create(null, { + label: { + configurable: true, + enumerable: true, + value: 'value' + } + }), + sources: ['key'], + samples: [{ + timestamp: 0, + value: 0 + }] + } + } + }) + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts new file mode 100644 index 00000000000..b3d49643fd0 --- /dev/null +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts @@ -0,0 +1,9 @@ +import { Command } from '@redis/client/dist/lib/RESP/types'; +import MRANGE_WITHLABELS_GROUPBY, { createMRangeWithLabelsGroupByTransformArguments } from './MRANGE_WITHLABELS_GROUPBY'; + +export default { + FIRST_KEY_INDEX: MRANGE_WITHLABELS_GROUPBY.FIRST_KEY_INDEX, + IS_READ_ONLY: MRANGE_WITHLABELS_GROUPBY.IS_READ_ONLY, + transformArguments: createMRangeWithLabelsGroupByTransformArguments('TS.MREVRANGE'), + transformReply: MRANGE_WITHLABELS_GROUPBY.transformReply, +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/QUERYINDEX.spec.ts b/packages/time-series/lib/commands/QUERYINDEX.spec.ts index 010c5c8f639..74f201bb74b 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.spec.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.spec.ts @@ -1,34 +1,34 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './QUERYINDEX'; +import QUERYINDEX from './QUERYINDEX'; -describe('QUERYINDEX', () => { - describe('transformArguments', () => { - it('single filter', () => { - assert.deepEqual( - transformArguments('*'), - ['TS.QUERYINDEX', '*'] - ); - }); - - it('multiple filters', () => { - assert.deepEqual( - transformArguments(['a=1', 'b=2']), - ['TS.QUERYINDEX', 'a=1', 'b=2'] - ); - }); +describe('TS.QUERYINDEX', () => { + describe('transformArguments', () => { + it('single filter', () => { + assert.deepEqual( + QUERYINDEX.transformArguments('*'), + ['TS.QUERYINDEX', '*'] + ); }); - testUtils.testWithClient('client.ts.queryIndex', async client => { - await client.ts.create('key', { - LABELS: { - label: 'value' - } - }); + it('multiple filters', () => { + assert.deepEqual( + QUERYINDEX.transformArguments(['a=1', 'b=2']), + ['TS.QUERYINDEX', 'a=1', 'b=2'] + ); + }); + }); + + testUtils.testWithClient('client.ts.queryIndex', async client => { + const [, reply] = await Promise.all([ + client.ts.create('key', { + LABELS: { + label: 'value' + } + }), + client.ts.queryIndex('label=value') + ]); - assert.deepEqual( - await client.ts.queryIndex('label=value'), - ['key'] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, ['key']); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/QUERYINDEX.ts b/packages/time-series/lib/commands/QUERYINDEX.ts index 46eb5647040..86c2a3c5a7e 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.ts @@ -1,11 +1,14 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { Filter } from '.'; +import { ArrayReply, BlobStringReply, SetReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -export const IS_READ_ONLY = true; - -export function transformArguments(filter: Filter): RedisCommandArguments { - return pushVerdictArguments(['TS.QUERYINDEX'], filter); -} - -export declare function transformReply(): Array; +export default { + FIRST_KEY_INDEX: undefined, + IS_READ_ONLY: true, + transformArguments(filter: RedisVariadicArgument) { + return pushVariadicArguments(['TS.QUERYINDEX'], filter); + }, + transformReply: { + 2: undefined as unknown as () => ArrayReply, + 3: undefined as unknown as () => SetReply + } +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/RANGE.spec.ts b/packages/time-series/lib/commands/RANGE.spec.ts index 1e6a9958806..bc5d38d740b 100644 --- a/packages/time-series/lib/commands/RANGE.spec.ts +++ b/packages/time-series/lib/commands/RANGE.spec.ts @@ -1,38 +1,40 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RANGE'; -import { TimeSeriesAggregationType } from '.'; +import RANGE from './RANGE'; +import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; -describe('RANGE', () => { - it('transformArguments', () => { - assert.deepEqual( - transformArguments('key', '-', '+', { - FILTER_BY_TS: [0], - FILTER_BY_VALUE: { - min: 1, - max: 2 - }, - COUNT: 1, - ALIGN: '-', - AGGREGATION: { - type: TimeSeriesAggregationType.AVERAGE, - timeBucket: 1 - } - }), - ['TS.RANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', - '1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1'] - ); - }); +describe('TS.RANGE', () => { + it('transformArguments', () => { + assert.deepEqual( + RANGE.transformArguments('key', '-', '+', { + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 1, + max: 2 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.RANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', + '1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1' + ] + ); + }); - testUtils.testWithClient('client.ts.range', async client => { - await client.ts.add('key', 1, 2); + testUtils.testWithClient('client.ts.range', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 1, 2), + client.ts.range('key', '-', '+') + ]); - assert.deepEqual( - await client.ts.range('key', '-', '+'), - [{ - timestamp: 1, - value: 2 - }] - ); - }, GLOBAL.SERVERS.OPEN); + assert.deepEqual(reply, [{ + timestamp: 1, + value: 2 + }]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/RANGE.ts b/packages/time-series/lib/commands/RANGE.ts index e6ce256bbe6..084073fefe6 100644 --- a/packages/time-series/lib/commands/RANGE.ts +++ b/packages/time-series/lib/commands/RANGE.ts @@ -1,24 +1,119 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { RangeOptions, Timestamp, pushRangeArguments, SampleRawReply, SampleReply, transformRangeReply } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: string, - fromTimestamp: Timestamp, - toTimestamp: Timestamp, - options?: RangeOptions -): RedisCommandArguments { - return pushRangeArguments( - ['TS.RANGE', key], - fromTimestamp, - toTimestamp, - options +import { CommandArguments, RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { Timestamp, transformTimestampArgument, SamplesRawReply, transformSamplesReply } from '.'; +import { TimeSeriesAggregationType } from './CREATERULE'; +import { Resp2Reply } from '@redis/client/dist/lib/RESP/types'; + +export const TIME_SERIES_BUCKET_TIMESTAMP = { + LOW: '-', + MIDDLE: '~', + END: '+' +}; + +export type TimeSeriesBucketTimestamp = typeof TIME_SERIES_BUCKET_TIMESTAMP[keyof typeof TIME_SERIES_BUCKET_TIMESTAMP]; + +export interface TsRangeOptions { + LATEST?: boolean; + FILTER_BY_TS?: Array; + FILTER_BY_VALUE?: { + min: number; + max: number; + }; + COUNT?: number; + ALIGN?: Timestamp; + AGGREGATION?: { + ALIGN?: Timestamp; + type: TimeSeriesAggregationType; + timeBucket: Timestamp; + BUCKETTIMESTAMP?: TimeSeriesBucketTimestamp; + EMPTY?: boolean; + }; +} + +export function pushRangeArguments( + args: CommandArguments, + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + options?: TsRangeOptions +) { + args.push( + transformTimestampArgument(fromTimestamp), + transformTimestampArgument(toTimestamp) + ); + + if (options?.LATEST) { + args.push('LATEST'); + } + + if (options?.FILTER_BY_TS) { + args.push('FILTER_BY_TS'); + for (const timestamp of options.FILTER_BY_TS) { + args.push(transformTimestampArgument(timestamp)); + } + } + + if (options?.FILTER_BY_VALUE) { + args.push( + 'FILTER_BY_VALUE', + options.FILTER_BY_VALUE.min.toString(), + options.FILTER_BY_VALUE.max.toString() ); + } + + if (options?.COUNT !== undefined) { + args.push('COUNT', options.COUNT.toString()); + } + + if (options?.AGGREGATION) { + if (options?.ALIGN !== undefined) { + args.push('ALIGN', transformTimestampArgument(options.ALIGN)); + } + + args.push( + 'AGGREGATION', + options.AGGREGATION.type, + transformTimestampArgument(options.AGGREGATION.timeBucket) + ); + + if (options.AGGREGATION.BUCKETTIMESTAMP) { + args.push( + 'BUCKETTIMESTAMP', + options.AGGREGATION.BUCKETTIMESTAMP + ); + } + + if (options.AGGREGATION.EMPTY) { + args.push('EMPTY'); + } + } + + return args; } -export function transformReply(reply: Array): Array { - return transformRangeReply(reply); +export function transformRangeArguments( + command: RedisArgument, + key: RedisArgument, + fromTimestamp: Timestamp, + toTimestamp: Timestamp, + options?: TsRangeOptions +) { + return pushRangeArguments( + [command, key], + fromTimestamp, + toTimestamp, + options + ); } + +export default { + FIRST_KEY_INDEX: 1, + IS_READ_ONLY: true, + transformArguments: transformRangeArguments.bind(undefined, 'TS.RANGE'), + transformReply: { + 2(reply: Resp2Reply) { + return transformSamplesReply[2](reply); + }, + 3(reply: SamplesRawReply) { + return transformSamplesReply[3](reply); + } + } +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/REVRANGE.spec.ts b/packages/time-series/lib/commands/REVRANGE.spec.ts index ffd90268c81..c371e8306b0 100644 --- a/packages/time-series/lib/commands/REVRANGE.spec.ts +++ b/packages/time-series/lib/commands/REVRANGE.spec.ts @@ -1,106 +1,40 @@ -import { strict as assert } from 'assert'; +import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './REVRANGE'; -import { TimeSeriesAggregationType } from '.'; - -describe('REVRANGE', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - transformArguments('key', '-', '+'), - ['TS.REVRANGE', 'key', '-', '+'] - ); - }); - - it('with FILTER_BY_TS', () => { - assert.deepEqual( - transformArguments('key', '-', '+', { - FILTER_BY_TS: [0] - }), - ['TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_TS', '0'] - ); - }); - - it('with FILTER_BY_VALUE', () => { - assert.deepEqual( - transformArguments('key', '-', '+', { - FILTER_BY_VALUE: { - min: 1, - max: 2 - } - }), - ['TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_VALUE', '1', '2'] - ); - }); - - it('with COUNT', () => { - assert.deepEqual( - transformArguments('key', '-', '+', { - COUNT: 1 - }), - ['TS.REVRANGE', 'key', '-', '+', 'COUNT', '1'] - ); - }); - - it('with ALIGN', () => { - assert.deepEqual( - transformArguments('key', '-', '+', { - ALIGN: '-' - }), - ['TS.REVRANGE', 'key', '-', '+', 'ALIGN', '-'] - ); - }); - - it('with AGGREGATION', () => { - assert.deepEqual( - transformArguments('key', '-', '+', { - AGGREGATION: { - type: TimeSeriesAggregationType.AVERAGE, - timeBucket: 1 - } - }), - ['TS.REVRANGE', 'key', '-', '+', 'AGGREGATION', 'AVG', '1'] - ); - }); - - it('with FILTER_BY_TS, FILTER_BY_VALUE, COUNT, ALIGN, AGGREGATION', () => { - assert.deepEqual( - transformArguments('key', '-', '+', { - FILTER_BY_TS: [0], - FILTER_BY_VALUE: { - min: 1, - max: 2 - }, - COUNT: 1, - ALIGN: '-', - AGGREGATION: { - type: TimeSeriesAggregationType.AVERAGE, - timeBucket: 1 - } - }), - [ - 'TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', - '1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1' - ] - ); - }); - }); - - testUtils.testWithClient('client.ts.revRange', async client => { - await Promise.all([ - client.ts.add('key', 0, 1), - client.ts.add('key', 1, 2) - ]); - - assert.deepEqual( - await client.ts.revRange('key', '-', '+'), - [{ - timestamp: 1, - value: 2 - }, { - timestamp: 0, - value: 1 - }] - ); - }, GLOBAL.SERVERS.OPEN); +import REVRANGE from './REVRANGE'; +import { TIME_SERIES_AGGREGATION_TYPE } from '../index'; + +describe('TS.REVRANGE', () => { + it('transformArguments', () => { + assert.deepEqual( + REVRANGE.transformArguments('key', '-', '+', { + FILTER_BY_TS: [0], + FILTER_BY_VALUE: { + min: 1, + max: 2 + }, + COUNT: 1, + ALIGN: '-', + AGGREGATION: { + type: TIME_SERIES_AGGREGATION_TYPE.AVG, + timeBucket: 1 + } + }), + [ + 'TS.REVRANGE', 'key', '-', '+', 'FILTER_BY_TS', '0', 'FILTER_BY_VALUE', + '1', '2', 'COUNT', '1', 'ALIGN', '-', 'AGGREGATION', 'AVG', '1' + ] + ); + }); + + testUtils.testWithClient('client.ts.revRange', async client => { + const [, reply] = await Promise.all([ + client.ts.add('key', 1, 2), + client.ts.revRange('key', '-', '+') + ]); + + assert.deepEqual(reply, [{ + timestamp: 1, + value: 2 + }]); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/time-series/lib/commands/REVRANGE.ts b/packages/time-series/lib/commands/REVRANGE.ts index 9179756b5de..1097223080b 100644 --- a/packages/time-series/lib/commands/REVRANGE.ts +++ b/packages/time-series/lib/commands/REVRANGE.ts @@ -1,24 +1,9 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { RangeOptions, Timestamp, pushRangeArguments, SampleRawReply, SampleReply, transformRangeReply } from '.'; - -export const FIRST_KEY_INDEX = 1; - -export const IS_READ_ONLY = true; - -export function transformArguments( - key: string, - fromTimestamp: Timestamp, - toTimestamp: Timestamp, - options?: RangeOptions -): RedisCommandArguments { - return pushRangeArguments( - ['TS.REVRANGE', key], - fromTimestamp, - toTimestamp, - options - ); -} - -export function transformReply(reply: Array): Array { - return transformRangeReply(reply); -} +import { Command } from '@redis/client/dist/lib/RESP/types'; +import RANGE, { transformRangeArguments } from './RANGE'; + +export default { + FIRST_KEY_INDEX: RANGE.FIRST_KEY_INDEX, + IS_READ_ONLY: RANGE.IS_READ_ONLY, + transformArguments: transformRangeArguments.bind(undefined, 'TS.REVRANGE'), + transformReply: RANGE.transformReply +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/index.spec.ts b/packages/time-series/lib/commands/index.spec.ts index a29eefe860a..ff7f4afad68 100644 --- a/packages/time-series/lib/commands/index.spec.ts +++ b/packages/time-series/lib/commands/index.spec.ts @@ -1,439 +1,423 @@ -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { strict as assert } from 'assert'; -import { - transformTimestampArgument, - pushRetentionArgument, - TimeSeriesEncoding, - pushEncodingArgument, - pushChunkSizeArgument, - pushDuplicatePolicy, - pushLabelsArgument, - transformIncrDecrArguments, - transformSampleReply, - TimeSeriesAggregationType, - pushRangeArguments, - pushMRangeGroupByArguments, - TimeSeriesReducers, - pushFilterArgument, - pushMRangeArguments, - pushWithLabelsArgument, - pushMRangeWithLabelsArguments, - transformRangeReply, - transformMRangeReply, - transformMRangeWithLabelsReply, - TimeSeriesDuplicatePolicies, - pushLatestArgument, - TimeSeriesBucketTimestamp -} from '.'; - -describe('transformTimestampArgument', () => { - it('number', () => { - assert.equal( - transformTimestampArgument(0), - '0' - ); - }); - - it('Date', () => { - assert.equal( - transformTimestampArgument(new Date(0)), - '0' - ); - }); - - it('string', () => { - assert.equal( - transformTimestampArgument('*'), - '*' - ); - }); -}); - -function testOptionalArgument(fn: (args: RedisCommandArguments) => unknown): void { - it('undefined', () => { - assert.deepEqual( - fn([]), - [] - ); - }); -} - -describe('pushRetentionArgument', () => { - testOptionalArgument(pushRetentionArgument); - - it('number', () => { - assert.deepEqual( - pushRetentionArgument([], 1), - ['RETENTION', '1'] - ); - }); -}); - -describe('pushEncodingArgument', () => { - testOptionalArgument(pushEncodingArgument); - - it('UNCOMPRESSED', () => { - assert.deepEqual( - pushEncodingArgument([], TimeSeriesEncoding.UNCOMPRESSED), - ['ENCODING', 'UNCOMPRESSED'] - ); - }); -}); - -describe('pushChunkSizeArgument', () => { - testOptionalArgument(pushChunkSizeArgument); - - it('number', () => { - assert.deepEqual( - pushChunkSizeArgument([], 1), - ['CHUNK_SIZE', '1'] - ); - }); -}); - -describe('pushDuplicatePolicy', () => { - testOptionalArgument(pushDuplicatePolicy); - - it('BLOCK', () => { - assert.deepEqual( - pushDuplicatePolicy([], TimeSeriesDuplicatePolicies.BLOCK), - ['DUPLICATE_POLICY', 'BLOCK'] - ); - }); -}); - -describe('pushLabelsArgument', () => { - testOptionalArgument(pushLabelsArgument); - - it("{ label: 'value' }", () => { - assert.deepEqual( - pushLabelsArgument([], { label: 'value' }), - ['LABELS', 'label', 'value'] - ); - }); -}); - -describe('transformIncrDecrArguments', () => { - it('without options', () => { - assert.deepEqual( - transformIncrDecrArguments('TS.INCRBY', 'key', 1), - ['TS.INCRBY', 'key', '1'] - ); - }); - - it('with TIMESTAMP', () => { - assert.deepEqual( - transformIncrDecrArguments('TS.INCRBY', 'key', 1, { - TIMESTAMP: '*' - }), - ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*'] - ); - }); - - it('with UNCOMPRESSED', () => { - assert.deepEqual( - transformIncrDecrArguments('TS.INCRBY', 'key', 1, { - UNCOMPRESSED: true - }), - ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED'] - ); - }); - - it('with UNCOMPRESSED false', () => { - assert.deepEqual( - transformIncrDecrArguments('TS.INCRBY', 'key', 1, { - UNCOMPRESSED: false - }), - ['TS.INCRBY', 'key', '1'] - ); - }); -}); - -it('transformSampleReply', () => { - assert.deepEqual( - transformSampleReply([1, '1.1']), - { - timestamp: 1, - value: 1.1 - } - ); -}); - -describe('pushRangeArguments', () => { - it('without options', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+'), - ['-', '+'] - ); - }); - - describe('with FILTER_BY_TS', () => { - it('string', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+', { - FILTER_BY_TS: ['ts'] - }), - ['-', '+', 'FILTER_BY_TS', 'ts'] - ); - }); - - it('Array', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+', { - FILTER_BY_TS: ['1', '2'] - }), - ['-', '+', 'FILTER_BY_TS', '1', '2'] - ); - }); - }); - - it('with FILTER_BY_VALUE', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+', { - FILTER_BY_VALUE: { - min: 1, - max: 2 - } - }), - ['-', '+', 'FILTER_BY_VALUE', '1', '2'] - ); - }); - - it('with COUNT', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+', { - COUNT: 1 - }), - ['-', '+', 'COUNT', '1'] - ); - }); - - it('with ALIGN', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+', { - ALIGN: 1 - }), - ['-', '+', 'ALIGN', '1'] - ); - }); - - describe('with AGGREGATION', () => { - it('without options', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+', { - AGGREGATION: { - type: TimeSeriesAggregationType.FIRST, - timeBucket: 1 - } - }), - ['-', '+', 'AGGREGATION', 'FIRST', '1'] - ); - }); - - it('with BUCKETTIMESTAMP', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+', { - AGGREGATION: { - type: TimeSeriesAggregationType.FIRST, - timeBucket: 1, - BUCKETTIMESTAMP: TimeSeriesBucketTimestamp.LOW - } - }), - ['-', '+', 'AGGREGATION', 'FIRST', '1', 'BUCKETTIMESTAMP', '-'] - ); - }); - - it('with BUCKETTIMESTAMP', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+', { - AGGREGATION: { - type: TimeSeriesAggregationType.FIRST, - timeBucket: 1, - EMPTY: true - } - }), - ['-', '+', 'AGGREGATION', 'FIRST', '1', 'EMPTY'] - ); - }); - }); - - it('with FILTER_BY_TS, FILTER_BY_VALUE, COUNT, ALIGN, AGGREGATION', () => { - assert.deepEqual( - pushRangeArguments([], '-', '+', { - FILTER_BY_TS: ['ts'], - FILTER_BY_VALUE: { - min: 1, - max: 2 - }, - COUNT: 1, - ALIGN: 1, - AGGREGATION: { - type: TimeSeriesAggregationType.FIRST, - timeBucket: 1, - BUCKETTIMESTAMP: TimeSeriesBucketTimestamp.LOW, - EMPTY: true - } - }), - ['-', '+', 'FILTER_BY_TS', 'ts', 'FILTER_BY_VALUE', '1', '2', - 'COUNT', '1', 'ALIGN', '1', 'AGGREGATION', 'FIRST', '1', 'BUCKETTIMESTAMP', '-', 'EMPTY'] - ); - }); -}); - -describe('pushMRangeGroupByArguments', () => { - it('undefined', () => { - assert.deepEqual( - pushMRangeGroupByArguments([]), - [] - ); - }); - - it('with GROUPBY', () => { - assert.deepEqual( - pushMRangeGroupByArguments([], { - label: 'label', - reducer: TimeSeriesReducers.MAXIMUM - }), - ['GROUPBY', 'label', 'REDUCE', 'MAX'] - ); - }); -}); - -describe('pushFilterArgument', () => { - it('string', () => { - assert.deepEqual( - pushFilterArgument([], 'label=value'), - ['FILTER', 'label=value'] - ); - }); - - it('Array', () => { - assert.deepEqual( - pushFilterArgument([], ['1=1', '2=2']), - ['FILTER', '1=1', '2=2'] - ); - }); -}); - -describe('pushMRangeArguments', () => { - it('without options', () => { - assert.deepEqual( - pushMRangeArguments([], '-', '+', 'label=value'), - ['-', '+', 'FILTER', 'label=value'] - ); - }); - - it('with GROUPBY', () => { - assert.deepEqual( - pushMRangeArguments([], '-', '+', 'label=value', { - GROUPBY: { - label: 'label', - reducer: TimeSeriesReducers.MAXIMUM - } - }), - ['-', '+', 'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'MAX'] - ); - }); -}); - -describe('pushWithLabelsArgument', () => { - it('without selected labels', () => { - assert.deepEqual( - pushWithLabelsArgument([]), - ['WITHLABELS'] - ); - }); - - it('with selected labels', () => { - assert.deepEqual( - pushWithLabelsArgument([], ['label']), - ['SELECTED_LABELS', 'label'] - ); - }); -}); - -it('pushMRangeWithLabelsArguments', () => { - assert.deepEqual( - pushMRangeWithLabelsArguments([], '-', '+', 'label=value'), - ['-', '+', 'WITHLABELS', 'FILTER', 'label=value'] - ); -}); - -it('transformRangeReply', () => { - assert.deepEqual( - transformRangeReply([[1, '1.1'], [2, '2.2']]), - [{ - timestamp: 1, - value: 1.1 - }, { - timestamp: 2, - value: 2.2 - }] - ); -}); - -describe('transformMRangeReply', () => { - assert.deepEqual( - transformMRangeReply([[ - 'key', - [], - [[1, '1.1'], [2, '2.2']] - ]]), - [{ - key: 'key', - samples: [{ - timestamp: 1, - value: 1.1 - }, { - timestamp: 2, - value: 2.2 - }] - }] - ); -}); - -describe('transformMRangeWithLabelsReply', () => { - assert.deepEqual( - transformMRangeWithLabelsReply([[ - 'key', - [['label', 'value']], - [[1, '1.1'], [2, '2.2']] - ]]), - [{ - key: 'key', - labels: { - label: 'value' - }, - samples: [{ - timestamp: 1, - value: 1.1 - }, { - timestamp: 2, - value: 2.2 - }] - }] - ); -}); - -describe('pushLatestArgument', () => { - it('undefined', () => { - assert.deepEqual( - pushLatestArgument([]), - [] - ); - }); - - it('false', () => { - assert.deepEqual( - pushLatestArgument([], false), - [] - ); - }); - - it('true', () => { - assert.deepEqual( - pushLatestArgument([], true), - ['LATEST'] - ); - }); -}) +// import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +// import { strict as assert } from 'node:assert'; +// import { +// transformTimestampArgument, +// pushRetentionArgument, +// TimeSeriesEncoding, +// pushEncodingArgument, +// pushChunkSizeArgument, +// pushDuplicatePolicy, +// pushLabelsArgument, +// transformIncrDecrArguments, +// transformSampleReply, +// TimeSeriesAggregationType, +// pushRangeArguments, +// pushMRangeGroupByArguments, +// TimeSeriesReducers, +// pushFilterArgument, +// pushMRangeArguments, +// pushWithLabelsArgument, +// pushMRangeWithLabelsArguments, +// transformRangeReply, +// transformMRangeReply, +// transformMRangeWithLabelsReply, +// TimeSeriesDuplicatePolicies, +// pushLatestArgument, +// TimeSeriesBucketTimestamp +// } from '.'; + +// describe('transformTimestampArgument', () => { +// it('number', () => { +// assert.equal( +// transformTimestampArgument(0), +// '0' +// ); +// }); + +// it('Date', () => { +// assert.equal( +// transformTimestampArgument(new Date(0)), +// '0' +// ); +// }); + +// it('string', () => { +// assert.equal( +// transformTimestampArgument('*'), +// '*' +// ); +// }); +// }); + +// function testOptionalArgument(fn: (args: RedisCommandArguments) => unknown): void { +// it('undefined', () => { +// assert.deepEqual( +// fn([]), +// [] +// ); +// }); +// } + +// describe('pushRetentionArgument', () => { +// testOptionalArgument(pushRetentionArgument); + +// it('number', () => { +// assert.deepEqual( +// pushRetentionArgument([], 1), +// ['RETENTION', '1'] +// ); +// }); +// }); + +// describe('pushEncodingArgument', () => { +// testOptionalArgument(pushEncodingArgument); + +// it('UNCOMPRESSED', () => { +// assert.deepEqual( +// pushEncodingArgument([], TimeSeriesEncoding.UNCOMPRESSED), +// ['ENCODING', 'UNCOMPRESSED'] +// ); +// }); +// }); + +// describe('pushChunkSizeArgument', () => { +// testOptionalArgument(pushChunkSizeArgument); + +// it('number', () => { +// assert.deepEqual( +// pushChunkSizeArgument([], 1), +// ['CHUNK_SIZE', '1'] +// ); +// }); +// }); + +// describe('pushDuplicatePolicy', () => { +// testOptionalArgument(pushDuplicatePolicy); + +// it('BLOCK', () => { +// assert.deepEqual( +// pushDuplicatePolicy([], TimeSeriesDuplicatePolicies.BLOCK), +// ['DUPLICATE_POLICY', 'BLOCK'] +// ); +// }); +// }); + +// describe('pushLabelsArgument', () => { +// testOptionalArgument(pushLabelsArgument); + +// it("{ label: 'value' }", () => { +// assert.deepEqual( +// pushLabelsArgument([], { label: 'value' }), +// ['LABELS', 'label', 'value'] +// ); +// }); +// }); + +// describe('transformIncrDecrArguments', () => { +// it('without options', () => { +// assert.deepEqual( +// transformIncrDecrArguments('TS.INCRBY', 'key', 1), +// ['TS.INCRBY', 'key', '1'] +// ); +// }); + +// it('with TIMESTAMP', () => { +// assert.deepEqual( +// transformIncrDecrArguments('TS.INCRBY', 'key', 1, { +// TIMESTAMP: '*' +// }), +// ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*'] +// ); +// }); + +// it('with UNCOMPRESSED', () => { +// assert.deepEqual( +// transformIncrDecrArguments('TS.INCRBY', 'key', 1, { +// UNCOMPRESSED: true +// }), +// ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED'] +// ); +// }); + +// it('with UNCOMPRESSED false', () => { +// assert.deepEqual( +// transformIncrDecrArguments('TS.INCRBY', 'key', 1, { +// UNCOMPRESSED: false +// }), +// ['TS.INCRBY', 'key', '1'] +// ); +// }); +// }); + +// it('transformSampleReply', () => { +// assert.deepEqual( +// transformSampleReply([1, '1.1']), +// { +// timestamp: 1, +// value: 1.1 +// } +// ); +// }); + +// describe('pushRangeArguments', () => { +// it('without options', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+'), +// ['-', '+'] +// ); +// }); + +// describe('with FILTER_BY_TS', () => { +// it('string', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+', { +// FILTER_BY_TS: ['ts'] +// }), +// ['-', '+', 'FILTER_BY_TS', 'ts'] +// ); +// }); + +// it('Array', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+', { +// FILTER_BY_TS: ['1', '2'] +// }), +// ['-', '+', 'FILTER_BY_TS', '1', '2'] +// ); +// }); +// }); + +// it('with FILTER_BY_VALUE', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+', { +// FILTER_BY_VALUE: { +// min: 1, +// max: 2 +// } +// }), +// ['-', '+', 'FILTER_BY_VALUE', '1', '2'] +// ); +// }); + +// it('with COUNT', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+', { +// COUNT: 1 +// }), +// ['-', '+', 'COUNT', '1'] +// ); +// }); + +// it('with ALIGN', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+', { +// ALIGN: 1 +// }), +// ['-', '+', 'ALIGN', '1'] +// ); +// }); + +// describe('with AGGREGATION', () => { +// it('without options', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+', { +// AGGREGATION: { +// type: TimeSeriesAggregationType.FIRST, +// timeBucket: 1 +// } +// }), +// ['-', '+', 'AGGREGATION', 'FIRST', '1'] +// ); +// }); + +// it('with BUCKETTIMESTAMP', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+', { +// AGGREGATION: { +// type: TimeSeriesAggregationType.FIRST, +// timeBucket: 1, +// BUCKETTIMESTAMP: TimeSeriesBucketTimestamp.LOW +// } +// }), +// ['-', '+', 'AGGREGATION', 'FIRST', '1', 'BUCKETTIMESTAMP', '-'] +// ); +// }); + +// it('with BUCKETTIMESTAMP', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+', { +// AGGREGATION: { +// type: TimeSeriesAggregationType.FIRST, +// timeBucket: 1, +// EMPTY: true +// } +// }), +// ['-', '+', 'AGGREGATION', 'FIRST', '1', 'EMPTY'] +// ); +// }); +// }); + +// it('with FILTER_BY_TS, FILTER_BY_VALUE, COUNT, ALIGN, AGGREGATION', () => { +// assert.deepEqual( +// pushRangeArguments([], '-', '+', { +// FILTER_BY_TS: ['ts'], +// FILTER_BY_VALUE: { +// min: 1, +// max: 2 +// }, +// COUNT: 1, +// ALIGN: 1, +// AGGREGATION: { +// type: TimeSeriesAggregationType.FIRST, +// timeBucket: 1, +// BUCKETTIMESTAMP: TimeSeriesBucketTimestamp.LOW, +// EMPTY: true +// } +// }), +// ['-', '+', 'FILTER_BY_TS', 'ts', 'FILTER_BY_VALUE', '1', '2', +// 'COUNT', '1', 'ALIGN', '1', 'AGGREGATION', 'FIRST', '1', 'BUCKETTIMESTAMP', '-', 'EMPTY'] +// ); +// }); +// }); + +// describe('pushMRangeGroupByArguments', () => { +// it('undefined', () => { +// assert.deepEqual( +// pushMRangeGroupByArguments([]), +// [] +// ); +// }); + +// it('with GROUPBY', () => { +// assert.deepEqual( +// pushMRangeGroupByArguments([], { +// label: 'label', +// reducer: TimeSeriesReducers.MAXIMUM +// }), +// ['GROUPBY', 'label', 'REDUCE', 'MAX'] +// ); +// }); +// }); + +// describe('pushFilterArgument', () => { +// it('string', () => { +// assert.deepEqual( +// pushFilterArgument([], 'label=value'), +// ['FILTER', 'label=value'] +// ); +// }); + +// it('Array', () => { +// assert.deepEqual( +// pushFilterArgument([], ['1=1', '2=2']), +// ['FILTER', '1=1', '2=2'] +// ); +// }); +// }); + +// describe('pushMRangeArguments', () => { +// it('without options', () => { +// assert.deepEqual( +// pushMRangeArguments([], '-', '+', 'label=value'), +// ['-', '+', 'FILTER', 'label=value'] +// ); +// }); + +// it('with GROUPBY', () => { +// assert.deepEqual( +// pushMRangeArguments([], '-', '+', 'label=value', { +// GROUPBY: { +// label: 'label', +// reducer: TimeSeriesReducers.MAXIMUM +// } +// }), +// ['-', '+', 'FILTER', 'label=value', 'GROUPBY', 'label', 'REDUCE', 'MAX'] +// ); +// }); +// }); + +// it('pushMRangeWithLabelsArguments', () => { +// assert.deepEqual( +// pushMRangeWithLabelsArguments([], '-', '+', 'label=value'), +// ['-', '+', 'WITHLABELS', 'FILTER', 'label=value'] +// ); +// }); + +// it('transformRangeReply', () => { +// assert.deepEqual( +// transformRangeReply([[1, '1.1'], [2, '2.2']]), +// [{ +// timestamp: 1, +// value: 1.1 +// }, { +// timestamp: 2, +// value: 2.2 +// }] +// ); +// }); + +// describe('transformMRangeReply', () => { +// assert.deepEqual( +// transformMRangeReply([[ +// 'key', +// [], +// [[1, '1.1'], [2, '2.2']] +// ]]), +// [{ +// key: 'key', +// samples: [{ +// timestamp: 1, +// value: 1.1 +// }, { +// timestamp: 2, +// value: 2.2 +// }] +// }] +// ); +// }); + +// describe('transformMRangeWithLabelsReply', () => { +// assert.deepEqual( +// transformMRangeWithLabelsReply([[ +// 'key', +// [['label', 'value']], +// [[1, '1.1'], [2, '2.2']] +// ]]), +// [{ +// key: 'key', +// labels: { +// label: 'value' +// }, +// samples: [{ +// timestamp: 1, +// value: 1.1 +// }, { +// timestamp: 2, +// value: 2.2 +// }] +// }] +// ); +// }); + +// describe('pushLatestArgument', () => { +// it('undefined', () => { +// assert.deepEqual( +// pushLatestArgument([]), +// [] +// ); +// }); + +// it('false', () => { +// assert.deepEqual( +// pushLatestArgument([], false), +// [] +// ); +// }); + +// it('true', () => { +// assert.deepEqual( +// pushLatestArgument([], true), +// ['LATEST'] +// ); +// }); +// }) diff --git a/packages/time-series/lib/commands/index.ts b/packages/time-series/lib/commands/index.ts index ca382498060..5b9d97b6566 100644 --- a/packages/time-series/lib/commands/index.ts +++ b/packages/time-series/lib/commands/index.ts @@ -1,473 +1,399 @@ -import * as ADD from './ADD'; -import * as ALTER from './ALTER'; -import * as CREATE from './CREATE'; -import * as CREATERULE from './CREATERULE'; -import * as DECRBY from './DECRBY'; -import * as DEL from './DEL'; -import * as DELETERULE from './DELETERULE'; -import * as GET from './GET'; -import * as INCRBY from './INCRBY'; -import * as INFO_DEBUG from './INFO_DEBUG'; -import * as INFO from './INFO'; -import * as MADD from './MADD'; -import * as MGET from './MGET'; -import * as MGET_WITHLABELS from './MGET_WITHLABELS'; -import * as QUERYINDEX from './QUERYINDEX'; -import * as RANGE from './RANGE'; -import * as REVRANGE from './REVRANGE'; -import * as MRANGE from './MRANGE'; -import * as MRANGE_WITHLABELS from './MRANGE_WITHLABELS'; -import * as MREVRANGE from './MREVRANGE'; -import * as MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS'; -import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; -import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import type { DoubleReply, NumberReply, RedisArgument, RedisCommands, TuplesReply, UnwrapReply, Resp2Reply, ArrayReply, BlobStringReply, MapReply, NullReply, TypeMapping, ReplyUnion, RespType } from '@redis/client/lib/RESP/types'; +import ADD, { TsIgnoreOptions } from './ADD'; +import ALTER from './ALTER'; +import CREATE from './CREATE'; +import CREATERULE from './CREATERULE'; +import DECRBY from './DECRBY'; +import DEL from './DEL'; +import DELETERULE from './DELETERULE'; +import GET from './GET'; +import INCRBY from './INCRBY'; +import INFO_DEBUG from './INFO_DEBUG'; +import INFO from './INFO'; +import MADD from './MADD'; +import MGET_SELECTED_LABELS from './MGET_SELECTED_LABELS'; +import MGET_WITHLABELS from './MGET_WITHLABELS'; +import MGET from './MGET'; +import MRANGE_GROUPBY from './MRANGE_GROUPBY'; +import MRANGE_SELECTED_LABELS_GROUPBY from './MRANGE_SELECTED_LABELS_GROUPBY'; +import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS'; +import MRANGE_WITHLABELS_GROUPBY from './MRANGE_WITHLABELS_GROUPBY'; +import MRANGE_WITHLABELS from './MRANGE_WITHLABELS'; +import MRANGE from './MRANGE'; +import MREVRANGE_GROUPBY from './MREVRANGE_GROUPBY'; +import MREVRANGE_SELECTED_LABELS_GROUPBY from './MREVRANGE_SELECTED_LABELS_GROUPBY'; +import MREVRANGE_SELECTED_LABELS from './MREVRANGE_SELECTED_LABELS'; +import MREVRANGE_WITHLABELS_GROUPBY from './MREVRANGE_WITHLABELS_GROUPBY'; +import MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS'; +import MREVRANGE from './MREVRANGE'; +import QUERYINDEX from './QUERYINDEX'; +import RANGE from './RANGE'; +import REVRANGE from './REVRANGE'; +import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/lib/commands/generic-transformers'; +import { RESP_TYPES } from '@redis/client/lib/RESP/decoder'; export default { - ADD, - add: ADD, - ALTER, - alter: ALTER, - CREATE, - create: CREATE, - CREATERULE, - createRule: CREATERULE, - DECRBY, - decrBy: DECRBY, - DEL, - del: DEL, - DELETERULE, - deleteRule: DELETERULE, - GET, - get: GET, - INCRBY, - incrBy: INCRBY, - INFO_DEBUG, - infoDebug: INFO_DEBUG, - INFO, - info: INFO, - MADD, - mAdd: MADD, - MGET, - mGet: MGET, - MGET_WITHLABELS, - mGetWithLabels: MGET_WITHLABELS, - QUERYINDEX, - queryIndex: QUERYINDEX, - RANGE, - range: RANGE, - REVRANGE, - revRange: REVRANGE, - MRANGE, - mRange: MRANGE, - MRANGE_WITHLABELS, - mRangeWithLabels: MRANGE_WITHLABELS, - MREVRANGE, - mRevRange: MREVRANGE, - MREVRANGE_WITHLABELS, - mRevRangeWithLabels: MREVRANGE_WITHLABELS -}; - -export enum TimeSeriesAggregationType { - AVG = 'AVG', - // @deprecated - AVERAGE = 'AVG', - FIRST = 'FIRST', - LAST = 'LAST', - MIN = 'MIN', - // @deprecated - MINIMUM = 'MIN', - MAX = 'MAX', - // @deprecated - MAXIMUM = 'MAX', - SUM = 'SUM', - RANGE = 'RANGE', - COUNT = 'COUNT', - STD_P = 'STD.P', - STD_S = 'STD.S', - VAR_P = 'VAR.P', - VAR_S = 'VAR.S', - TWA = 'TWA' + ADD, + add: ADD, + ALTER, + alter: ALTER, + CREATE, + create: CREATE, + CREATERULE, + createRule: CREATERULE, + DECRBY, + decrBy: DECRBY, + DEL, + del: DEL, + DELETERULE, + deleteRule: DELETERULE, + GET, + get: GET, + INCRBY, + incrBy: INCRBY, + INFO_DEBUG, + infoDebug: INFO_DEBUG, + INFO, + info: INFO, + MADD, + mAdd: MADD, + MGET_SELECTED_LABELS, + mGetSelectedLabels: MGET_SELECTED_LABELS, + MGET_WITHLABELS, + mGetWithLabels: MGET_WITHLABELS, + MGET, + mGet: MGET, + MRANGE_GROUPBY, + mRangeGroupBy: MRANGE_GROUPBY, + MRANGE_SELECTED_LABELS_GROUPBY, + mRangeSelectedLabelsGroupBy: MRANGE_SELECTED_LABELS_GROUPBY, + MRANGE_SELECTED_LABELS, + mRangeSelectedLabels: MRANGE_SELECTED_LABELS, + MRANGE_WITHLABELS_GROUPBY, + mRangeWithLabelsGroupBy: MRANGE_WITHLABELS_GROUPBY, + MRANGE_WITHLABELS, + mRangeWithLabels: MRANGE_WITHLABELS, + MRANGE, + mRange: MRANGE, + MREVRANGE_GROUPBY, + mRevRangeGroupBy: MREVRANGE_GROUPBY, + MREVRANGE_SELECTED_LABELS_GROUPBY, + mRevRangeSelectedLabelsGroupBy: MREVRANGE_SELECTED_LABELS_GROUPBY, + MREVRANGE_SELECTED_LABELS, + mRevRangeSelectedLabels: MREVRANGE_SELECTED_LABELS, + MREVRANGE_WITHLABELS_GROUPBY, + mRevRangeWithLabelsGroupBy: MREVRANGE_WITHLABELS_GROUPBY, + MREVRANGE_WITHLABELS, + mRevRangeWithLabels: MREVRANGE_WITHLABELS, + MREVRANGE, + mRevRange: MREVRANGE, + QUERYINDEX, + queryIndex: QUERYINDEX, + RANGE, + range: RANGE, + REVRANGE, + revRange: REVRANGE +} as const satisfies RedisCommands; + +export function pushIgnoreArgument(args: Array, ignore?: TsIgnoreOptions) { + if (ignore !== undefined) { + args.push('IGNORE', ignore.maxTimeDiff.toString(), ignore.maxValDiff.toString()); + } } -export enum TimeSeriesDuplicatePolicies { - BLOCK = 'BLOCK', - FIRST = 'FIRST', - LAST = 'LAST', - MIN = 'MIN', - MAX = 'MAX', - SUM = 'SUM' +export function pushRetentionArgument(args: Array, retention?: number) { + if (retention !== undefined) { + args.push('RETENTION', retention.toString()); + } } -export enum TimeSeriesReducers { - AVG = 'AVG', - SUM = 'SUM', - MIN = 'MIN', - // @deprecated - MINIMUM = 'MIN', - MAX = 'MAX', - // @deprecated - MAXIMUM = 'MAX', - RANGE = 'range', - COUNT = 'COUNT', - STD_P = 'STD.P', - STD_S = 'STD.S', - VAR_P = 'VAR.P', - VAR_S = 'VAR.S', -} +export const TIME_SERIES_ENCODING = { + COMPRESSED: 'COMPRESSED', + UNCOMPRESSED: 'UNCOMPRESSED' +} as const; -export type Timestamp = number | Date | string; +export type TimeSeriesEncoding = typeof TIME_SERIES_ENCODING[keyof typeof TIME_SERIES_ENCODING]; -export function transformTimestampArgument(timestamp: Timestamp): string { - if (typeof timestamp === 'string') return timestamp; - - return ( - typeof timestamp === 'number' ? - timestamp : - timestamp.getTime() - ).toString(); -} - -export function pushIgnoreArgument(args: RedisCommandArguments, ignore?: ADD.TsIgnoreOptions) { - if (ignore !== undefined) { - args.push('IGNORE', ignore.MAX_TIME_DIFF.toString(), ignore.MAX_VAL_DIFF.toString()); +export function pushEncodingArgument(args: Array, encoding?: TimeSeriesEncoding) { + if (encoding !== undefined) { + args.push('ENCODING', encoding); } } -export function pushRetentionArgument(args: RedisCommandArguments, retention?: number): RedisCommandArguments { - if (retention !== undefined) { - args.push( - 'RETENTION', - retention.toString() - ); - } - - return args; +export function pushChunkSizeArgument(args: Array, chunkSize?: number) { + if (chunkSize !== undefined) { + args.push('CHUNK_SIZE', chunkSize.toString()); + } } -export enum TimeSeriesEncoding { - COMPRESSED = 'COMPRESSED', - UNCOMPRESSED = 'UNCOMPRESSED' -} +export const TIME_SERIES_DUPLICATE_POLICIES = { + BLOCK: 'BLOCK', + FIRST: 'FIRST', + LAST: 'LAST', + MIN: 'MIN', + MAX: 'MAX', + SUM: 'SUM' +} as const; -export function pushEncodingArgument(args: RedisCommandArguments, encoding?: TimeSeriesEncoding): RedisCommandArguments { - if (encoding !== undefined) { - args.push( - 'ENCODING', - encoding - ); - } +export type TimeSeriesDuplicatePolicies = typeof TIME_SERIES_DUPLICATE_POLICIES[keyof typeof TIME_SERIES_DUPLICATE_POLICIES]; - return args; +export function pushDuplicatePolicy(args: Array, duplicatePolicy?: TimeSeriesDuplicatePolicies) { + if (duplicatePolicy !== undefined) { + args.push('DUPLICATE_POLICY', duplicatePolicy); + } } -export function pushChunkSizeArgument(args: RedisCommandArguments, chunkSize?: number): RedisCommandArguments { - if (chunkSize !== undefined) { - args.push( - 'CHUNK_SIZE', - chunkSize.toString() - ); - } - - return args; -} +export type Timestamp = number | Date | string; -export function pushDuplicatePolicy(args: RedisCommandArguments, duplicatePolicy?: TimeSeriesDuplicatePolicies): RedisCommandArguments { - if (duplicatePolicy !== undefined) { - args.push( - 'DUPLICATE_POLICY', - duplicatePolicy - ); - } +export function transformTimestampArgument(timestamp: Timestamp): string { + if (typeof timestamp === 'string') return timestamp; - return args; + return ( + typeof timestamp === 'number' ? + timestamp : + timestamp.getTime() + ).toString(); } -export type RawLabels = Array<[label: string, value: string]>; - export type Labels = { - [label: string]: string; + [label: string]: string; }; -export function transformLablesReply(reply: RawLabels): Labels { - const labels: Labels = {}; +export function pushLabelsArgument(args: Array, labels?: Labels) { + if (labels) { + args.push('LABELS'); - for (const [key, value] of reply) { - labels[key] = value; + for (const [label, value] of Object.entries(labels)) { + args.push(label, value); } + } - return labels -} - -export function pushLabelsArgument(args: RedisCommandArguments, labels?: Labels): RedisCommandArguments { - if (labels) { - args.push('LABELS'); - - for (const [label, value] of Object.entries(labels)) { - args.push(label, value); - } - } - - return args; -} - -export interface IncrDecrOptions { - TIMESTAMP?: Timestamp; - RETENTION?: number; - UNCOMPRESSED?: boolean; - CHUNK_SIZE?: number; - LABELS?: Labels; -} - -export function transformIncrDecrArguments( - command: 'TS.INCRBY' | 'TS.DECRBY', - key: string, - value: number, - options?: IncrDecrOptions -): RedisCommandArguments { - const args = [ - command, - key, - value.toString() - ]; - - if (options?.TIMESTAMP !== undefined && options?.TIMESTAMP !== null) { - args.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP)); - } - - pushRetentionArgument(args, options?.RETENTION); - - if (options?.UNCOMPRESSED) { - args.push('UNCOMPRESSED'); - } - - pushChunkSizeArgument(args, options?.CHUNK_SIZE); - - pushLabelsArgument(args, options?.LABELS); - - return args; + return args; } -export type SampleRawReply = [timestamp: number, value: string]; - -export interface SampleReply { - timestamp: number; - value: number; -} +export type SampleRawReply = TuplesReply<[timestamp: NumberReply, value: DoubleReply]>; -export function transformSampleReply(reply: SampleRawReply): SampleReply { +export const transformSampleReply = { + 2(reply: Resp2Reply) { + const [ timestamp, value ] = reply as unknown as UnwrapReply; return { - timestamp: reply[0], - value: Number(reply[1]) - }; -} - -export enum TimeSeriesBucketTimestamp { - LOW = '-', - HIGH = '+', - MID = '~' -} - -export interface RangeOptions { - LATEST?: boolean; - FILTER_BY_TS?: Array; - FILTER_BY_VALUE?: { - min: number; - max: number; + timestamp, + value: Number(value) // TODO: use double type mapping instead }; - COUNT?: number; - ALIGN?: Timestamp; - AGGREGATION?: { - type: TimeSeriesAggregationType; - timeBucket: Timestamp; - BUCKETTIMESTAMP?: TimeSeriesBucketTimestamp; - EMPTY?: boolean; + }, + 3(reply: SampleRawReply) { + const [ timestamp, value ] = reply as unknown as UnwrapReply; + return { + timestamp, + value }; -} - -export function pushRangeArguments( - args: RedisCommandArguments, - fromTimestamp: Timestamp, - toTimestamp: Timestamp, - options?: RangeOptions -): RedisCommandArguments { - args.push( - transformTimestampArgument(fromTimestamp), - transformTimestampArgument(toTimestamp) - ); + } +}; - pushLatestArgument(args, options?.LATEST); +export type SamplesRawReply = ArrayReply; - if (options?.FILTER_BY_TS) { - args.push('FILTER_BY_TS'); - for (const ts of options.FILTER_BY_TS) { - args.push(transformTimestampArgument(ts)); - } - } +export const transformSamplesReply = { + 2(reply: Resp2Reply) { + return (reply as unknown as UnwrapReply) + .map(sample => transformSampleReply[2](sample)); + }, + 3(reply: SamplesRawReply) { + return (reply as unknown as UnwrapReply) + .map(sample => transformSampleReply[3](sample)); + } +}; - if (options?.FILTER_BY_VALUE) { - args.push( - 'FILTER_BY_VALUE', - options.FILTER_BY_VALUE.min.toString(), - options.FILTER_BY_VALUE.max.toString() - ); +// TODO: move to @redis/client? +export function resp2MapToValue< + RAW_VALUE extends TuplesReply<[key: BlobStringReply, ...rest: Array]>, + TRANSFORMED +>( + wrappedReply: ArrayReply, + parseFunc: (rawValue: UnwrapReply) => TRANSFORMED, + typeMapping?: TypeMapping +): MapReply { + const reply = wrappedReply as unknown as UnwrapReply; + switch (typeMapping?.[RESP_TYPES.MAP]) { + case Map: { + const ret = new Map(); + for (const wrappedTuple of reply) { + const tuple = wrappedTuple as unknown as UnwrapReply; + const key = tuple[0] as unknown as UnwrapReply; + ret.set(key.toString(), parseFunc(tuple)); + } + return ret as never; } - - if (options?.COUNT) { - args.push( - 'COUNT', - options.COUNT.toString() - ); + case Array: { + for (const wrappedTuple of reply) { + const tuple = wrappedTuple as unknown as UnwrapReply; + (tuple[1] as unknown as TRANSFORMED) = parseFunc(tuple); + } + return reply as never; } - - if (options?.ALIGN) { - args.push( - 'ALIGN', - transformTimestampArgument(options.ALIGN) - ); + default: { + const ret: Record = Object.create(null); + for (const wrappedTuple of reply) { + const tuple = wrappedTuple as unknown as UnwrapReply; + const key = tuple[0] as unknown as UnwrapReply; + ret[key.toString()] = parseFunc(tuple); + } + return ret as never; } - - if (options?.AGGREGATION) { - args.push( - 'AGGREGATION', - options.AGGREGATION.type, - transformTimestampArgument(options.AGGREGATION.timeBucket) - ); - - if (options.AGGREGATION.BUCKETTIMESTAMP) { - args.push( - 'BUCKETTIMESTAMP', - options.AGGREGATION.BUCKETTIMESTAMP - ); - } - - if (options.AGGREGATION.EMPTY) { - args.push('EMPTY'); - } + } +} + +export function resp3MapToValue< + RAW_VALUE extends RespType, // TODO: simplify types + TRANSFORMED +>( + wrappedReply: MapReply, + parseFunc: (rawValue: UnwrapReply) => TRANSFORMED +): MapReply { + const reply = wrappedReply as unknown as UnwrapReply; + if (reply instanceof Array) { + for (let i = 1; i < reply.length; i += 2) { + (reply[i] as unknown as TRANSFORMED) = parseFunc(reply[i] as unknown as UnwrapReply); } - - return args; -} - -interface MRangeGroupBy { - label: string; - reducer: TimeSeriesReducers; -} - -export function pushMRangeGroupByArguments(args: RedisCommandArguments, groupBy?: MRangeGroupBy): RedisCommandArguments { - if (groupBy) { - args.push( - 'GROUPBY', - groupBy.label, - 'REDUCE', - groupBy.reducer - ); + } else if (reply instanceof Map) { + for (const [key, value] of reply.entries()) { + (reply as unknown as Map).set( + key, + parseFunc(value as unknown as UnwrapReply) + ); } - - return args; -} - -export type Filter = string | Array; - -export function pushFilterArgument(args: RedisCommandArguments, filter: string | Array): RedisCommandArguments { - args.push('FILTER'); - return pushVerdictArguments(args, filter); -} - -export interface MRangeOptions extends RangeOptions { - GROUPBY?: MRangeGroupBy; -} - -export function pushMRangeArguments( - args: RedisCommandArguments, - fromTimestamp: Timestamp, - toTimestamp: Timestamp, - filter: Filter, - options?: MRangeOptions -): RedisCommandArguments { - args = pushRangeArguments(args, fromTimestamp, toTimestamp, options); - args = pushFilterArgument(args, filter); - return pushMRangeGroupByArguments(args, options?.GROUPBY); -} - -export type SelectedLabels = string | Array; - -export function pushWithLabelsArgument(args: RedisCommandArguments, selectedLabels?: SelectedLabels): RedisCommandArguments { - if (!selectedLabels) { - args.push('WITHLABELS'); - } else { - args.push('SELECTED_LABELS'); - args = pushVerdictArguments(args, selectedLabels); + } else { + for (const [key, value] of Object.entries(reply)) { + (reply[key] as unknown as TRANSFORMED) = parseFunc(value as unknown as UnwrapReply); } - - return args; -} - -export interface MRangeWithLabelsOptions extends MRangeOptions { - SELECTED_LABELS?: SelectedLabels; -} - -export function pushMRangeWithLabelsArguments( - args: RedisCommandArguments, - fromTimestamp: Timestamp, - toTimestamp: Timestamp, - filter: Filter, - options?: MRangeWithLabelsOptions -): RedisCommandArguments { - args = pushRangeArguments(args, fromTimestamp, toTimestamp, options); - args = pushWithLabelsArgument(args, options?.SELECTED_LABELS); - args = pushFilterArgument(args, filter); - return pushMRangeGroupByArguments(args, options?.GROUPBY); + } + return reply as never; +} + +export function pushSelectedLabelsArguments( + args: Array, + selectedLabels: RedisVariadicArgument +) { + args.push('SELECTED_LABELS'); + return pushVariadicArguments(args, selectedLabels); +} + +export type RawLabelValue = BlobStringReply | NullReply; + +export type RawLabels = ArrayReply>; + +export function transformRESP2Labels( + labels: RawLabels, + typeMapping?: TypeMapping +): MapReply { + const unwrappedLabels = labels as unknown as UnwrapReply; + switch (typeMapping?.[RESP_TYPES.MAP]) { + case Map: + const map = new Map(); + for (const tuple of unwrappedLabels) { + const [key, value] = tuple as unknown as UnwrapReply; + const unwrappedKey = key as unknown as UnwrapReply; + map.set(unwrappedKey.toString(), value); + } + return map as never; + + case Array: + return unwrappedLabels.flat() as never; + + case Object: + default: + const labelsObject: Record = Object.create(null); + for (const tuple of unwrappedLabels) { + const [key, value] = tuple as unknown as UnwrapReply; + const unwrappedKey = key as unknown as UnwrapReply; + labelsObject[unwrappedKey.toString()] = value; + } + return labelsObject as never; + } } -export function transformRangeReply(reply: Array): Array { - return reply.map(transformSampleReply); -} +export function transformRESP2LabelsWithSources( + labels: RawLabels, + typeMapping?: TypeMapping +) { + const unwrappedLabels = labels as unknown as UnwrapReply; + const to = unwrappedLabels.length - 2; // ignore __reducer__ and __source__ + let transformedLabels: MapReply; + switch (typeMapping?.[RESP_TYPES.MAP]) { + case Map: + const map = new Map(); + for (let i = 0; i < to; i++) { + const [key, value] = unwrappedLabels[i] as unknown as UnwrapReply; + const unwrappedKey = key as unknown as UnwrapReply; + map.set(unwrappedKey.toString(), value); + } + transformedLabels = map as never; + break; + + case Array: + transformedLabels = unwrappedLabels.slice(0, to).flat() as never; + break; + + case Object: + default: + const labelsObject: Record = Object.create(null); + for (let i = 0; i < to; i++) { + const [key, value] = unwrappedLabels[i] as unknown as UnwrapReply; + const unwrappedKey = key as unknown as UnwrapReply; + labelsObject[unwrappedKey.toString()] = value; + } + transformedLabels = labelsObject as never; + break; + } -type MRangeRawReply = Array<[ - key: string, - labels: RawLabels, - samples: Array -]>; + const sourcesTuple = unwrappedLabels[unwrappedLabels.length - 1]; + const unwrappedSourcesTuple = sourcesTuple as unknown as UnwrapReply; + // the __source__ label will never be null + const transformedSources = transformRESP2Sources(unwrappedSourcesTuple[1] as BlobStringReply); -interface MRangeReplyItem { - key: string; - samples: Array; + return { + labels: transformedLabels, + sources: transformedSources + }; } -export function transformMRangeReply(reply: MRangeRawReply): Array { - const args = []; - - for (const [key, _, sample] of reply) { - args.push({ - key, - samples: sample.map(transformSampleReply) - }); - } - - return args; -} -export interface MRangeWithLabelsReplyItem extends MRangeReplyItem { - labels: Labels; -} +function transformRESP2Sources(sourcesRaw: BlobStringReply) { + // if a label contains "," this function will produce incorrcet results.. + // there is not much we can do about it, and we assume most users won't be using "," in their labels.. + + const unwrappedSources = sourcesRaw as unknown as UnwrapReply; + if (typeof unwrappedSources === 'string') { + return unwrappedSources.split(','); + } -export function transformMRangeWithLabelsReply(reply: MRangeRawReply): Array { - const args = []; + const indexOfComma = unwrappedSources.indexOf(','); + if (indexOfComma === -1) { + return [unwrappedSources]; + } - for (const [key, labels, samples] of reply) { - args.push({ - key, - labels: transformLablesReply(labels), - samples: samples.map(transformSampleReply) - }); + const sourcesArray = [ + unwrappedSources.subarray(0, indexOfComma) + ]; + + let previousComma = indexOfComma + 1; + while (true) { + const indexOf = unwrappedSources.indexOf(',', previousComma); + if (indexOf === -1) { + sourcesArray.push( + unwrappedSources.subarray(previousComma) + ); + break; } - return args; -} - -export function pushLatestArgument(args: RedisCommandArguments, latest?: boolean): RedisCommandArguments { - if (latest) { - args.push('LATEST'); - } + const source = unwrappedSources.subarray( + previousComma, + indexOf + ); + sourcesArray.push(source); + previousComma = indexOf + 1; + } - return args; + return sourcesArray; } diff --git a/packages/time-series/lib/index.ts b/packages/time-series/lib/index.ts index 6002556ca1a..bd0be1e9cea 100644 --- a/packages/time-series/lib/index.ts +++ b/packages/time-series/lib/index.ts @@ -1,9 +1,7 @@ -export { default } from './commands'; - export { - TimeSeriesDuplicatePolicies, - TimeSeriesEncoding, - TimeSeriesAggregationType, - TimeSeriesReducers, - TimeSeriesBucketTimestamp + default, + TIME_SERIES_ENCODING, TimeSeriesEncoding, + TIME_SERIES_DUPLICATE_POLICIES, TimeSeriesDuplicatePolicies } from './commands'; +export { TIME_SERIES_AGGREGATION_TYPE, TimeSeriesAggregationType } from './commands/CREATERULE'; +export { TIME_SERIES_BUCKET_TIMESTAMP, TimeSeriesBucketTimestamp } from './commands/RANGE'; diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index 6d534ccccef..1cb5c8ed97b 100644 --- a/packages/time-series/lib/test-utils.ts +++ b/packages/time-series/lib/test-utils.ts @@ -2,19 +2,20 @@ import TestUtils from '@redis/test-utils'; import TimeSeries from '.'; export default new TestUtils({ - dockerImageName: 'redislabs/redistimeseries', - dockerImageVersionArgument: 'timeseries-version' + dockerImageName: 'redis/redis-stack', + dockerImageVersionArgument: 'timeseries-version', + defaultDockerVersion: '7.4.0-v1' }); export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: ['--loadmodule /usr/lib/redis/modules/redistimeseries.so'], - clientOptions: { - modules: { - ts: TimeSeries - } - } + SERVERS: { + OPEN: { + serverArguments: [], + clientOptions: { + modules: { + ts: TimeSeries } + } } + } }; diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 65ee1e99c23..e0317fd231f 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,30 +1,24 @@ { "name": "@redis/time-series", - "version": "1.1.0", + "version": "2.0.0-next.2", "license": "MIT", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/lib/index.js", + "types": "./dist/lib/index.d.ts", "files": [ - "dist/" + "dist/", + "!dist/tsconfig.tsbuildinfo" ], "scripts": { - "test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'", - "build": "tsc", - "documentation": "typedoc" + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^1.0.0" + "@redis/client": "^2.0.0-next.4" }, "devDependencies": { - "@istanbuljs/nyc-config-typescript": "^1.0.2", - "@redis/test-utils": "*", - "@types/node": "^20.6.2", - "nyc": "^15.1.0", - "release-it": "^16.1.5", - "source-map-support": "^0.5.21", - "ts-node": "^10.9.1", - "typedoc": "^0.25.1", - "typescript": "^5.2.2" + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" }, "repository": { "type": "git", diff --git a/tsconfig.base.json b/tsconfig.base.json index 1157be947b9..bd2bcac0845 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,13 +1,20 @@ { - "extends": "@tsconfig/node14/tsconfig.json", "compilerOptions": { + "lib": ["ES2023"], + "module": "NodeNext", + "moduleResolution": "NodeNext", + "target": "ES2022", + + "strict": true, + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "esModuleInterop": true, + "skipLibCheck": true, + + "composite": true, + "sourceMap": true, "declaration": true, - "allowJs": true, - "useDefineForClassFields": true, - "esModuleInterop": false, - "resolveJsonModule": true - }, - "ts-node": { - "files": true + "declarationMap": true, + "allowJs": true } } diff --git a/tsconfig.json b/tsconfig.json index 285b7ff0a97..a578fefa54f 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,20 @@ { - "extends": "./tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": [ - "./index.ts" - ] + "files": [], + "references": [{ + "path": "./packages/client" + }, { + "path": "./packages/test-utils" + }, { + "path": "./packages/bloom" + }, { + "path": "./packages/graph" + }, { + "path": "./packages/json" + }, { + "path": "./packages/search" + }, { + "path": "./packages/time-series" + }, { + "path": "./packages/redis" + }] } From c07b4db7db47f68f5710f9361936a8ee1ccfd186 Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Tue, 15 Oct 2024 18:10:35 +0300 Subject: [PATCH 002/105] fix generating docs (#2853) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c626d4a48e8..7ab2a557ff2 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "test": "npm run test -ws --if-present", "build": "tsc --build", - "documentation": "typedoc", + "documentation": "typedoc --out ./documentation", "gh-pages": "gh-pages -d ./documentation -e ./documentation -u 'documentation-bot '" }, "devDependencies": { From dca39e14021eb1bbe1ae5eadeea5e9de692e47a0 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:30:06 +0200 Subject: [PATCH 003/105] Release client@5.0.0-next.5 --- packages/client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/package.json b/packages/client/package.json index cb82f67bd53..9d028aa2bb2 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "2.0.0-next.4", + "version": "5.0.0-next.5", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From accc5c47a875512782e17c49caf5f6f59ee9acf1 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:34:46 +0200 Subject: [PATCH 004/105] Updated the Bloom package to use client@5.0.0-next.5 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 850ad802a5d..26b580d8933 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" }, "devDependencies": { "@redis/test-utils": "*" From 4bb8cf52091b7cbd248dfa93d3910a21e90f14d4 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:36:26 +0200 Subject: [PATCH 005/105] Release bloom@5.0.0-next.5 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 26b580d8933..cbcbc8ce2bd 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "2.0.0-next.3", + "version": "5.0.0-next.5", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From c7b40889ac5e6cd53d1d02465fdb712b1ddafbdd Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:37:31 +0200 Subject: [PATCH 006/105] Updated the Graph package to use client@5.0.0-next.5 --- packages/graph/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graph/package.json b/packages/graph/package.json index 54b6aad6493..a1417829155 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -12,7 +12,7 @@ "test-disable": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" }, "devDependencies": { "@redis/test-utils": "*" From 875ed36d3a21c60d2d8cb3fae4cf1099d163d7d3 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:38:48 +0200 Subject: [PATCH 007/105] Release graph@5.0.0-next.5 --- packages/graph/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graph/package.json b/packages/graph/package.json index a1417829155..c62abb23892 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -1,6 +1,6 @@ { "name": "@redis/graph", - "version": "2.0.0-next.2", + "version": "5.0.0-next.5", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From a4466d2049d3e769f4930f8d35b2d21b8ed25795 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:40:01 +0200 Subject: [PATCH 008/105] Updated the JSON package to use client@5.0.0-next.5 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index 53711a5c0b6..0da4d3c683d 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" }, "devDependencies": { "@redis/test-utils": "*" From 91476f9ba758200b317c80f0d56407d7cd222126 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:40:46 +0200 Subject: [PATCH 009/105] Release json@5.0.0-next.5 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index 0da4d3c683d..6cc43916497 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "2.0.0-next.2", + "version": "5.0.0-next.5", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From c0a418140eb4fb147a05ce7647f0f440ec426070 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:42:24 +0200 Subject: [PATCH 010/105] Updated the Search package to use client@5.0.0-next.5 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index ea6c8a6b4c2..d891a19fa10 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" }, "devDependencies": { "@redis/test-utils": "*" From d3e720ffbd507963f0969a829f4200d4307ce02c Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:43:16 +0200 Subject: [PATCH 011/105] Release search@5.0.0-next.5 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index d891a19fa10..c5b75110f0f 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "2.0.0-next.2", + "version": "5.0.0-next.5", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 9fbd346a88fecf1ab6c0fd00661548022f3677d1 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:45:22 +0200 Subject: [PATCH 012/105] Updated the TimeSeries package to use client@5.0.0-next.5 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index e0317fd231f..b1592f33966 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" }, "devDependencies": { "@redis/test-utils": "*" From e01f8496f9b406168998c6a24db1524e6143cc9b Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:46:09 +0200 Subject: [PATCH 013/105] Release time-series@5.0.0-next.5 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index b1592f33966..1e41ee237c3 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "2.0.0-next.2", + "version": "5.0.0-next.5", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 78a7f32e24970bb656402dcebfde4bd91476b97a Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:48:26 +0200 Subject: [PATCH 014/105] Updated the main package to use the 5.0.0-next.5 sub-packages --- packages/redis/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/redis/package.json b/packages/redis/package.json index 0603e6c9480..8c90e50d95a 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -10,12 +10,12 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { - "@redis/bloom": "2.0.0-next.3", - "@redis/client": "2.0.0-next.4", - "@redis/graph": "2.0.0-next.2", - "@redis/json": "2.0.0-next.2", - "@redis/search": "2.0.0-next.2", - "@redis/time-series": "2.0.0-next.2" + "@redis/bloom": "5.0.0-next.5", + "@redis/client": "5.0.0-next.5", + "@redis/graph": "5.0.0-next.5", + "@redis/json": "5.0.0-next.5", + "@redis/search": "5.0.0-next.5", + "@redis/time-series": "5.0.0-next.5" }, "engines": { "node": ">= 18" From 5ace34b9c958e7ee5b65c88c46cb43dafe4e66ec Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Tue, 15 Oct 2024 17:51:47 +0200 Subject: [PATCH 015/105] Release redis@5.0.0-next.5 --- packages/redis/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/redis/package.json b/packages/redis/package.json index 8c90e50d95a..617a5bff357 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.0.0-next.4", + "version": "5.0.0-next.5", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 4708736f3b7a93fca2694dcd7a02f7d1cc99d789 Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Thu, 31 Oct 2024 18:16:59 +0200 Subject: [PATCH 016/105] new "transform arguments" API for better key and metadata extraction (#2733) * Parser support with all commands * remove "dist" from all imports for consistency * address most of my review comments * small tweak to multi type mapping handling * tweak multi commands / fix addScript cases * nits * addressed all in person review comments * revert addCommand/addScript changes to multi-commands addCommand needs to be there for sendCommand like ability within a multi. If its there, it might as well be used by createCommand() et al, to avoid repeating code. addScript is there (even though only used once), but now made private to keep the logic for bookkeeping near each other. --- package-lock.json | 2511 +++++------------ packages/bloom/lib/commands/bloom/ADD.spec.ts | 3 +- packages/bloom/lib/commands/bloom/ADD.ts | 12 +- .../bloom/lib/commands/bloom/CARD.spec.ts | 3 +- packages/bloom/lib/commands/bloom/CARD.ts | 9 +- .../bloom/lib/commands/bloom/EXISTS.spec.ts | 3 +- packages/bloom/lib/commands/bloom/EXISTS.ts | 12 +- .../bloom/lib/commands/bloom/INFO.spec.ts | 3 +- packages/bloom/lib/commands/bloom/INFO.ts | 17 +- .../bloom/lib/commands/bloom/INSERT.spec.ts | 15 +- packages/bloom/lib/commands/bloom/INSERT.ts | 28 +- .../lib/commands/bloom/LOADCHUNK.spec.ts | 3 +- .../bloom/lib/commands/bloom/LOADCHUNK.ts | 10 +- .../bloom/lib/commands/bloom/MADD.spec.ts | 3 +- packages/bloom/lib/commands/bloom/MADD.ts | 14 +- .../bloom/lib/commands/bloom/MEXISTS.spec.ts | 3 +- packages/bloom/lib/commands/bloom/MEXISTS.ts | 14 +- .../bloom/lib/commands/bloom/RESERVE.spec.ts | 9 +- packages/bloom/lib/commands/bloom/RESERVE.ts | 17 +- .../bloom/lib/commands/bloom/SCANDUMP.spec.ts | 3 +- packages/bloom/lib/commands/bloom/SCANDUMP.ts | 10 +- packages/bloom/lib/commands/bloom/index.ts | 2 +- .../commands/count-min-sketch/INCRBY.spec.ts | 5 +- .../lib/commands/count-min-sketch/INCRBY.ts | 20 +- .../commands/count-min-sketch/INFO.spec.ts | 3 +- .../lib/commands/count-min-sketch/INFO.ts | 9 +- .../count-min-sketch/INITBYDIM.spec.ts | 3 +- .../commands/count-min-sketch/INITBYDIM.ts | 10 +- .../count-min-sketch/INITBYPROB.spec.ts | 3 +- .../commands/count-min-sketch/INITBYPROB.ts | 10 +- .../commands/count-min-sketch/MERGE.spec.ts | 5 +- .../lib/commands/count-min-sketch/MERGE.ts | 24 +- .../commands/count-min-sketch/QUERY.spec.ts | 3 +- .../lib/commands/count-min-sketch/QUERY.ts | 12 +- .../lib/commands/count-min-sketch/index.ts | 2 +- .../bloom/lib/commands/cuckoo/ADD.spec.ts | 3 +- packages/bloom/lib/commands/cuckoo/ADD.ts | 12 +- .../bloom/lib/commands/cuckoo/ADDNX.spec.ts | 3 +- packages/bloom/lib/commands/cuckoo/ADDNX.ts | 12 +- .../bloom/lib/commands/cuckoo/COUNT.spec.ts | 3 +- packages/bloom/lib/commands/cuckoo/COUNT.ts | 10 +- .../bloom/lib/commands/cuckoo/DEL.spec.ts | 3 +- packages/bloom/lib/commands/cuckoo/DEL.ts | 12 +- .../bloom/lib/commands/cuckoo/EXISTS.spec.ts | 3 +- packages/bloom/lib/commands/cuckoo/EXISTS.ts | 12 +- .../bloom/lib/commands/cuckoo/INFO.spec.ts | 3 +- packages/bloom/lib/commands/cuckoo/INFO.ts | 9 +- .../bloom/lib/commands/cuckoo/INSERT.spec.ts | 3 +- packages/bloom/lib/commands/cuckoo/INSERT.ts | 25 +- .../lib/commands/cuckoo/INSERTNX.spec.ts | 3 +- .../bloom/lib/commands/cuckoo/INSERTNX.ts | 10 +- .../lib/commands/cuckoo/LOADCHUNK.spec.ts | 3 +- .../bloom/lib/commands/cuckoo/LOADCHUNK.ts | 10 +- .../bloom/lib/commands/cuckoo/RESERVE.spec.ts | 9 +- packages/bloom/lib/commands/cuckoo/RESERVE.ts | 19 +- .../lib/commands/cuckoo/SCANDUMP.spec.ts | 3 +- .../bloom/lib/commands/cuckoo/SCANDUMP.ts | 10 +- packages/bloom/lib/commands/cuckoo/index.ts | 2 +- .../bloom/lib/commands/t-digest/ADD.spec.ts | 3 +- packages/bloom/lib/commands/t-digest/ADD.ts | 13 +- .../lib/commands/t-digest/BYRANK.spec.ts | 3 +- .../bloom/lib/commands/t-digest/BYRANK.ts | 19 +- .../lib/commands/t-digest/BYREVRANK.spec.ts | 3 +- .../bloom/lib/commands/t-digest/BYREVRANK.ts | 8 +- .../bloom/lib/commands/t-digest/CDF.spec.ts | 3 +- packages/bloom/lib/commands/t-digest/CDF.ts | 15 +- .../lib/commands/t-digest/CREATE.spec.ts | 5 +- .../bloom/lib/commands/t-digest/CREATE.ts | 13 +- .../bloom/lib/commands/t-digest/INFO.spec.ts | 3 +- packages/bloom/lib/commands/t-digest/INFO.ts | 9 +- .../bloom/lib/commands/t-digest/MAX.spec.ts | 3 +- packages/bloom/lib/commands/t-digest/MAX.ts | 11 +- .../bloom/lib/commands/t-digest/MERGE.spec.ts | 9 +- packages/bloom/lib/commands/t-digest/MERGE.ts | 19 +- .../bloom/lib/commands/t-digest/MIN.spec.ts | 3 +- packages/bloom/lib/commands/t-digest/MIN.ts | 11 +- .../lib/commands/t-digest/QUANTILE.spec.ts | 3 +- .../bloom/lib/commands/t-digest/QUANTILE.ts | 15 +- .../bloom/lib/commands/t-digest/RANK.spec.ts | 3 +- packages/bloom/lib/commands/t-digest/RANK.ts | 17 +- .../bloom/lib/commands/t-digest/RESET.spec.ts | 3 +- packages/bloom/lib/commands/t-digest/RESET.ts | 9 +- .../lib/commands/t-digest/REVRANK.spec.ts | 3 +- .../bloom/lib/commands/t-digest/REVRANK.ts | 8 +- .../commands/t-digest/TRIMMED_MEAN.spec.ts | 3 +- .../lib/commands/t-digest/TRIMMED_MEAN.ts | 18 +- packages/bloom/lib/commands/t-digest/index.ts | 2 +- packages/bloom/lib/commands/top-k/ADD.spec.ts | 3 +- packages/bloom/lib/commands/top-k/ADD.ts | 12 +- .../bloom/lib/commands/top-k/COUNT.spec.ts | 3 +- packages/bloom/lib/commands/top-k/COUNT.ts | 12 +- .../bloom/lib/commands/top-k/INCRBY.spec.ts | 5 +- packages/bloom/lib/commands/top-k/INCRBY.ts | 20 +- .../bloom/lib/commands/top-k/INFO.spec.ts | 3 +- packages/bloom/lib/commands/top-k/INFO.ts | 11 +- .../bloom/lib/commands/top-k/LIST.spec.ts | 3 +- packages/bloom/lib/commands/top-k/LIST.ts | 9 +- .../lib/commands/top-k/LIST_WITHCOUNT.spec.ts | 3 +- .../lib/commands/top-k/LIST_WITHCOUNT.ts | 10 +- .../bloom/lib/commands/top-k/QUERY.spec.ts | 3 +- packages/bloom/lib/commands/top-k/QUERY.ts | 12 +- .../bloom/lib/commands/top-k/RESERVE.spec.ts | 5 +- packages/bloom/lib/commands/top-k/RESERVE.ts | 14 +- packages/bloom/lib/commands/top-k/index.ts | 2 +- packages/client/lib/RESP/encoder.ts | 2 +- packages/client/lib/RESP/types.ts | 9 +- packages/client/lib/client/commands-queue.ts | 8 +- packages/client/lib/client/index.spec.ts | 9 +- packages/client/lib/client/index.ts | 138 +- packages/client/lib/client/multi-command.ts | 70 +- packages/client/lib/client/parser.ts | 92 + packages/client/lib/client/pool.ts | 68 +- packages/client/lib/client/socket.ts | 2 +- packages/client/lib/cluster/index.ts | 162 +- packages/client/lib/cluster/multi-command.ts | 101 +- packages/client/lib/commander.ts | 6 +- packages/client/lib/commands/ACL_CAT.spec.ts | 5 +- packages/client/lib/commands/ACL_CAT.ts | 12 +- .../client/lib/commands/ACL_DELUSER.spec.ts | 5 +- packages/client/lib/commands/ACL_DELUSER.ts | 10 +- .../client/lib/commands/ACL_DRYRUN.spec.ts | 3 +- packages/client/lib/commands/ACL_DRYRUN.ts | 12 +- .../client/lib/commands/ACL_GENPASS.spec.ts | 5 +- packages/client/lib/commands/ACL_GENPASS.ts | 12 +- .../client/lib/commands/ACL_GETUSER.spec.ts | 3 +- packages/client/lib/commands/ACL_GETUSER.ts | 7 +- packages/client/lib/commands/ACL_LIST.spec.ts | 3 +- packages/client/lib/commands/ACL_LIST.ts | 7 +- packages/client/lib/commands/ACL_LOAD.spec.ts | 3 +- packages/client/lib/commands/ACL_LOAD.ts | 7 +- packages/client/lib/commands/ACL_LOG.spec.ts | 5 +- packages/client/lib/commands/ACL_LOG.ts | 14 +- .../client/lib/commands/ACL_LOG_RESET.spec.ts | 3 +- packages/client/lib/commands/ACL_LOG_RESET.ts | 7 +- packages/client/lib/commands/ACL_SAVE.spec.ts | 3 +- packages/client/lib/commands/ACL_SAVE.ts | 7 +- .../client/lib/commands/ACL_SETUSER.spec.ts | 5 +- packages/client/lib/commands/ACL_SETUSER.ts | 10 +- .../client/lib/commands/ACL_USERS.spec.ts | 3 +- packages/client/lib/commands/ACL_USERS.ts | 7 +- .../client/lib/commands/ACL_WHOAMI.spec.ts | 3 +- packages/client/lib/commands/ACL_WHOAMI.ts | 7 +- packages/client/lib/commands/APPEND.spec.ts | 3 +- packages/client/lib/commands/APPEND.ts | 7 +- packages/client/lib/commands/ASKING.spec.ts | 3 +- packages/client/lib/commands/ASKING.ts | 9 +- packages/client/lib/commands/AUTH.spec.ts | 5 +- packages/client/lib/commands/AUTH.ts | 15 +- .../client/lib/commands/BGREWRITEAOF.spec.ts | 3 +- packages/client/lib/commands/BGREWRITEAOF.ts | 7 +- packages/client/lib/commands/BGSAVE.spec.ts | 5 +- packages/client/lib/commands/BGSAVE.ts | 12 +- packages/client/lib/commands/BITCOUNT.spec.ts | 9 +- packages/client/lib/commands/BITCOUNT.ts | 19 +- packages/client/lib/commands/BITFIELD.spec.ts | 3 +- packages/client/lib/commands/BITFIELD.ts | 17 +- .../client/lib/commands/BITFIELD_RO.spec.ts | 5 +- packages/client/lib/commands/BITFIELD_RO.ts | 18 +- packages/client/lib/commands/BITOP.spec.ts | 5 +- packages/client/lib/commands/BITOP.ts | 11 +- packages/client/lib/commands/BITPOS.spec.ts | 11 +- packages/client/lib/commands/BITPOS.ts | 17 +- packages/client/lib/commands/BLMOVE.spec.ts | 3 +- packages/client/lib/commands/BLMOVE.ts | 16 +- packages/client/lib/commands/BLMPOP.spec.ts | 5 +- packages/client/lib/commands/BLMPOP.ts | 17 +- packages/client/lib/commands/BLPOP.spec.ts | 5 +- packages/client/lib/commands/BLPOP.ts | 15 +- packages/client/lib/commands/BRPOP.spec.ts | 5 +- packages/client/lib/commands/BRPOP.ts | 15 +- .../client/lib/commands/BRPOPLPUSH.spec.ts | 3 +- packages/client/lib/commands/BRPOPLPUSH.ts | 12 +- packages/client/lib/commands/BZMPOP.spec.ts | 5 +- packages/client/lib/commands/BZMPOP.ts | 9 +- packages/client/lib/commands/BZPOPMAX.spec.ts | 5 +- packages/client/lib/commands/BZPOPMAX.ts | 24 +- packages/client/lib/commands/BZPOPMIN.spec.ts | 5 +- packages/client/lib/commands/BZPOPMIN.ts | 11 +- .../lib/commands/CLIENT_CACHING.spec.ts | 5 +- .../client/lib/commands/CLIENT_CACHING.ts | 9 +- .../lib/commands/CLIENT_GETNAME.spec.ts | 3 +- .../client/lib/commands/CLIENT_GETNAME.ts | 10 +- .../lib/commands/CLIENT_GETREDIR.spec.ts | 3 +- .../client/lib/commands/CLIENT_GETREDIR.ts | 7 +- .../client/lib/commands/CLIENT_ID.spec.ts | 3 +- packages/client/lib/commands/CLIENT_ID.ts | 7 +- .../client/lib/commands/CLIENT_INFO.spec.ts | 3 +- packages/client/lib/commands/CLIENT_INFO.ts | 7 +- .../client/lib/commands/CLIENT_KILL.spec.ts | 23 +- packages/client/lib/commands/CLIENT_KILL.ts | 34 +- .../client/lib/commands/CLIENT_LIST.spec.ts | 7 +- packages/client/lib/commands/CLIENT_LIST.ts | 17 +- .../lib/commands/CLIENT_NO-EVICT.spec.ts | 5 +- .../client/lib/commands/CLIENT_NO-EVICT.ts | 9 +- .../lib/commands/CLIENT_NO-TOUCH.spec.ts | 5 +- .../client/lib/commands/CLIENT_NO-TOUCH.ts | 9 +- .../client/lib/commands/CLIENT_PAUSE.spec.ts | 5 +- packages/client/lib/commands/CLIENT_PAUSE.ts | 16 +- .../lib/commands/CLIENT_SETNAME.spec.ts | 3 +- .../client/lib/commands/CLIENT_SETNAME.ts | 7 +- .../lib/commands/CLIENT_TRACKING.spec.ts | 19 +- .../client/lib/commands/CLIENT_TRACKING.ts | 28 +- .../lib/commands/CLIENT_TRACKINGINFO.spec.ts | 3 +- .../lib/commands/CLIENT_TRACKINGINFO.ts | 7 +- .../lib/commands/CLIENT_UNPAUSE.spec.ts | 3 +- .../client/lib/commands/CLIENT_UNPAUSE.ts | 7 +- .../lib/commands/CLUSTER_ADDSLOTS.spec.ts | 5 +- .../client/lib/commands/CLUSTER_ADDSLOTS.ts | 12 +- .../commands/CLUSTER_ADDSLOTSRANGE.spec.ts | 5 +- .../lib/commands/CLUSTER_ADDSLOTSRANGE.ts | 13 +- .../lib/commands/CLUSTER_BUMPEPOCH.spec.ts | 3 +- .../client/lib/commands/CLUSTER_BUMPEPOCH.ts | 7 +- .../CLUSTER_COUNT-FAILURE-REPORTS.spec.ts | 3 +- .../commands/CLUSTER_COUNT-FAILURE-REPORTS.ts | 7 +- .../commands/CLUSTER_COUNTKEYSINSLOT.spec.ts | 3 +- .../lib/commands/CLUSTER_COUNTKEYSINSLOT.ts | 7 +- .../lib/commands/CLUSTER_DELSLOTS.spec.ts | 5 +- .../client/lib/commands/CLUSTER_DELSLOTS.ts | 12 +- .../commands/CLUSTER_DELSLOTSRANGE.spec.ts | 5 +- .../lib/commands/CLUSTER_DELSLOTSRANGE.ts | 13 +- .../lib/commands/CLUSTER_FAILOVER.spec.ts | 5 +- .../client/lib/commands/CLUSTER_FAILOVER.ts | 11 +- .../lib/commands/CLUSTER_FLUSHSLOTS.spec.ts | 3 +- .../client/lib/commands/CLUSTER_FLUSHSLOTS.ts | 7 +- .../lib/commands/CLUSTER_FORGET.spec.ts | 3 +- .../client/lib/commands/CLUSTER_FORGET.ts | 7 +- .../commands/CLUSTER_GETKEYSINSLOT.spec.ts | 3 +- .../lib/commands/CLUSTER_GETKEYSINSLOT.ts | 7 +- .../client/lib/commands/CLUSTER_INFO.spec.ts | 3 +- packages/client/lib/commands/CLUSTER_INFO.ts | 7 +- .../lib/commands/CLUSTER_KEYSLOT.spec.ts | 3 +- .../client/lib/commands/CLUSTER_KEYSLOT.ts | 7 +- .../client/lib/commands/CLUSTER_LINKS.spec.ts | 3 +- packages/client/lib/commands/CLUSTER_LINKS.ts | 7 +- .../client/lib/commands/CLUSTER_MEET.spec.ts | 3 +- packages/client/lib/commands/CLUSTER_MEET.ts | 7 +- .../client/lib/commands/CLUSTER_MYID.spec.ts | 3 +- packages/client/lib/commands/CLUSTER_MYID.ts | 7 +- .../lib/commands/CLUSTER_MYSHARDID.spec.ts | 3 +- .../client/lib/commands/CLUSTER_MYSHARDID.ts | 7 +- .../client/lib/commands/CLUSTER_NODES.spec.ts | 3 +- packages/client/lib/commands/CLUSTER_NODES.ts | 7 +- .../lib/commands/CLUSTER_REPLICAS.spec.ts | 3 +- .../client/lib/commands/CLUSTER_REPLICAS.ts | 7 +- .../lib/commands/CLUSTER_REPLICATE.spec.ts | 3 +- .../client/lib/commands/CLUSTER_REPLICATE.ts | 7 +- .../client/lib/commands/CLUSTER_RESET.spec.ts | 5 +- packages/client/lib/commands/CLUSTER_RESET.ts | 11 +- .../lib/commands/CLUSTER_SAVECONFIG.spec.ts | 3 +- .../client/lib/commands/CLUSTER_SAVECONFIG.ts | 7 +- .../commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts | 3 +- .../lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts | 7 +- .../lib/commands/CLUSTER_SETSLOT.spec.ts | 5 +- .../client/lib/commands/CLUSTER_SETSLOT.ts | 11 +- .../client/lib/commands/CLUSTER_SLOTS.spec.ts | 3 +- packages/client/lib/commands/CLUSTER_SLOTS.ts | 7 +- packages/client/lib/commands/COMMAND.ts | 6 +- .../client/lib/commands/COMMAND_COUNT.spec.ts | 3 +- packages/client/lib/commands/COMMAND_COUNT.ts | 7 +- .../lib/commands/COMMAND_GETKEYS.spec.ts | 3 +- .../client/lib/commands/COMMAND_GETKEYS.ts | 8 +- .../lib/commands/COMMAND_GETKEYSANDFLAGS.ts | 8 +- packages/client/lib/commands/COMMAND_INFO.ts | 7 +- .../client/lib/commands/COMMAND_LIST.spec.ts | 9 +- packages/client/lib/commands/COMMAND_LIST.ts | 11 +- .../client/lib/commands/CONFIG_GET.spec.ts | 5 +- packages/client/lib/commands/CONFIG_GET.ts | 10 +- .../lib/commands/CONFIG_RESETSTAT.spec.ts | 3 +- .../client/lib/commands/CONFIG_RESETSTAT.ts | 7 +- .../lib/commands/CONFIG_REWRITE.spec.ts | 3 +- .../client/lib/commands/CONFIG_REWRITE.ts | 7 +- .../client/lib/commands/CONFIG_SET.spec.ts | 5 +- packages/client/lib/commands/CONFIG_SET.ts | 14 +- packages/client/lib/commands/COPY.spec.ts | 9 +- packages/client/lib/commands/COPY.ts | 13 +- packages/client/lib/commands/DBSIZE.spec.ts | 3 +- packages/client/lib/commands/DBSIZE.ts | 7 +- packages/client/lib/commands/DECR.spec.ts | 3 +- packages/client/lib/commands/DECR.ts | 7 +- packages/client/lib/commands/DECRBY.spec.ts | 3 +- packages/client/lib/commands/DECRBY.ts | 8 +- packages/client/lib/commands/DEL.spec.ts | 5 +- packages/client/lib/commands/DEL.ts | 9 +- packages/client/lib/commands/DISCARD.spec.ts | 3 +- packages/client/lib/commands/DISCARD.ts | 5 +- packages/client/lib/commands/DUMP.spec.ts | 9 + packages/client/lib/commands/DUMP.ts | 7 +- packages/client/lib/commands/ECHO.spec.ts | 3 +- packages/client/lib/commands/ECHO.ts | 7 +- packages/client/lib/commands/EVAL.spec.ts | 3 +- packages/client/lib/commands/EVAL.ts | 22 +- packages/client/lib/commands/EVALSHA.spec.ts | 3 +- packages/client/lib/commands/EVALSHA.ts | 8 +- .../client/lib/commands/EVALSHA_RO.spec.ts | 3 +- packages/client/lib/commands/EVALSHA_RO.ts | 8 +- packages/client/lib/commands/EVAL_RO.spec.ts | 3 +- packages/client/lib/commands/EVAL_RO.ts | 8 +- packages/client/lib/commands/EXISTS.spec.ts | 7 +- packages/client/lib/commands/EXISTS.ts | 10 +- packages/client/lib/commands/EXPIRE.spec.ts | 5 +- packages/client/lib/commands/EXPIRE.ts | 14 +- packages/client/lib/commands/EXPIREAT.spec.ts | 7 +- packages/client/lib/commands/EXPIREAT.ts | 14 +- .../client/lib/commands/EXPIRETIME.spec.ts | 3 +- packages/client/lib/commands/EXPIRETIME.ts | 7 +- packages/client/lib/commands/FAILOVER.spec.ts | 13 +- packages/client/lib/commands/FAILOVER.ts | 15 +- packages/client/lib/commands/FCALL.spec.ts | 3 +- packages/client/lib/commands/FCALL.ts | 8 +- packages/client/lib/commands/FCALL_RO.spec.ts | 3 +- packages/client/lib/commands/FCALL_RO.ts | 8 +- packages/client/lib/commands/FLUSHALL.spec.ts | 7 +- packages/client/lib/commands/FLUSHALL.ts | 12 +- packages/client/lib/commands/FLUSHDB.spec.ts | 7 +- packages/client/lib/commands/FLUSHDB.ts | 12 +- .../lib/commands/FUNCTION_DELETE.spec.ts | 3 +- .../client/lib/commands/FUNCTION_DELETE.ts | 7 +- .../client/lib/commands/FUNCTION_DUMP.spec.ts | 3 +- packages/client/lib/commands/FUNCTION_DUMP.ts | 7 +- .../lib/commands/FUNCTION_FLUSH.spec.ts | 5 +- .../client/lib/commands/FUNCTION_FLUSH.ts | 13 +- .../client/lib/commands/FUNCTION_KILL.spec.ts | 3 +- packages/client/lib/commands/FUNCTION_KILL.ts | 7 +- .../client/lib/commands/FUNCTION_LIST.spec.ts | 5 +- packages/client/lib/commands/FUNCTION_LIST.ts | 13 +- .../commands/FUNCTION_LIST_WITHCODE.spec.ts | 5 +- .../lib/commands/FUNCTION_LIST_WITHCODE.ts | 9 +- .../client/lib/commands/FUNCTION_LOAD.spec.ts | 10 +- packages/client/lib/commands/FUNCTION_LOAD.ts | 15 +- .../lib/commands/FUNCTION_RESTORE.spec.ts | 5 +- .../client/lib/commands/FUNCTION_RESTORE.ts | 11 +- .../lib/commands/FUNCTION_STATS.spec.ts | 3 +- .../client/lib/commands/FUNCTION_STATS.ts | 7 +- packages/client/lib/commands/GEOADD.spec.ts | 13 +- packages/client/lib/commands/GEOADD.ts | 27 +- packages/client/lib/commands/GEODIST.spec.ts | 5 +- packages/client/lib/commands/GEODIST.ts | 13 +- packages/client/lib/commands/GEOHASH.spec.ts | 5 +- packages/client/lib/commands/GEOHASH.ts | 14 +- packages/client/lib/commands/GEOPOS.spec.ts | 5 +- packages/client/lib/commands/GEOPOS.ts | 14 +- .../client/lib/commands/GEORADIUS.spec.ts | 3 +- packages/client/lib/commands/GEORADIUS.ts | 27 +- .../lib/commands/GEORADIUSBYMEMBER.spec.ts | 3 +- .../client/lib/commands/GEORADIUSBYMEMBER.ts | 33 +- .../lib/commands/GEORADIUSBYMEMBER_RO.spec.ts | 3 +- .../lib/commands/GEORADIUSBYMEMBER_RO.ts | 10 +- .../GEORADIUSBYMEMBER_RO_WITH.spec.ts | 3 +- .../lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts | 10 +- .../commands/GEORADIUSBYMEMBER_STORE.spec.ts | 5 +- .../lib/commands/GEORADIUSBYMEMBER_STORE.ts | 18 +- .../commands/GEORADIUSBYMEMBER_WITH.spec.ts | 3 +- .../lib/commands/GEORADIUSBYMEMBER_WITH.ts | 41 +- .../client/lib/commands/GEORADIUS_RO.spec.ts | 3 +- packages/client/lib/commands/GEORADIUS_RO.ts | 9 +- .../lib/commands/GEORADIUS_RO_WITH.spec.ts | 3 +- .../client/lib/commands/GEORADIUS_RO_WITH.ts | 10 +- .../lib/commands/GEORADIUS_STORE.spec.ts | 5 +- .../client/lib/commands/GEORADIUS_STORE.ts | 19 +- .../lib/commands/GEORADIUS_WITH.spec.ts | 3 +- .../client/lib/commands/GEORADIUS_WITH.ts | 39 +- .../client/lib/commands/GEOSEARCH.spec.ts | 11 +- packages/client/lib/commands/GEOSEARCH.ts | 47 +- .../lib/commands/GEOSEARCHSTORE.spec.ts | 5 +- .../client/lib/commands/GEOSEARCHSTORE.ts | 14 +- .../lib/commands/GEOSEARCH_WITH.spec.ts | 3 +- .../client/lib/commands/GEOSEARCH_WITH.ts | 12 +- packages/client/lib/commands/GET.spec.ts | 3 +- packages/client/lib/commands/GET.ts | 8 +- packages/client/lib/commands/GETBIT.spec.ts | 5 +- packages/client/lib/commands/GETBIT.ts | 9 +- packages/client/lib/commands/GETDEL.spec.ts | 3 +- packages/client/lib/commands/GETDEL.ts | 7 +- packages/client/lib/commands/GETEX.spec.ts | 21 +- packages/client/lib/commands/GETEX.ts | 25 +- packages/client/lib/commands/GETRANGE.spec.ts | 5 +- packages/client/lib/commands/GETRANGE.ts | 9 +- packages/client/lib/commands/GETSET.spec.ts | 3 +- packages/client/lib/commands/GETSET.ts | 8 +- packages/client/lib/commands/HDEL.spec.ts | 5 +- packages/client/lib/commands/HDEL.ts | 10 +- packages/client/lib/commands/HELLO.spec.ts | 11 +- packages/client/lib/commands/HELLO.ts | 13 +- packages/client/lib/commands/HEXISTS.spec.ts | 5 +- packages/client/lib/commands/HEXISTS.ts | 9 +- packages/client/lib/commands/HEXPIRE.spec.ts | 7 +- packages/client/lib/commands/HEXPIRE.ts | 30 +- .../client/lib/commands/HEXPIREAT.spec.ts | 9 +- packages/client/lib/commands/HEXPIREAT.ts | 32 +- .../client/lib/commands/HEXPIRETIME.spec.ts | 5 +- packages/client/lib/commands/HEXPIRETIME.ts | 15 +- packages/client/lib/commands/HGET.spec.ts | 3 +- packages/client/lib/commands/HGET.ts | 9 +- packages/client/lib/commands/HGETALL.ts | 8 +- packages/client/lib/commands/HINCRBY.spec.ts | 3 +- packages/client/lib/commands/HINCRBY.ts | 14 +- .../client/lib/commands/HINCRBYFLOAT.spec.ts | 3 +- packages/client/lib/commands/HINCRBYFLOAT.ts | 14 +- packages/client/lib/commands/HKEYS.spec.ts | 3 +- packages/client/lib/commands/HKEYS.ts | 8 +- packages/client/lib/commands/HLEN.spec.ts | 3 +- packages/client/lib/commands/HLEN.ts | 8 +- packages/client/lib/commands/HMGET.spec.ts | 7 +- packages/client/lib/commands/HMGET.ts | 14 +- packages/client/lib/commands/HPERSIST.spec.ts | 5 +- packages/client/lib/commands/HPERSIST.ts | 16 +- packages/client/lib/commands/HPEXPIRE.spec.ts | 7 +- packages/client/lib/commands/HPEXPIRE.ts | 25 +- .../client/lib/commands/HPEXPIREAT.spec.ts | 9 +- packages/client/lib/commands/HPEXPIREAT.ts | 18 +- .../client/lib/commands/HPEXPIRETIME.spec.ts | 5 +- packages/client/lib/commands/HPEXPIRETIME.ts | 15 +- packages/client/lib/commands/HPTTL.spec.ts | 5 +- packages/client/lib/commands/HPTTL.ts | 15 +- .../client/lib/commands/HRANDFIELD.spec.ts | 3 +- packages/client/lib/commands/HRANDFIELD.ts | 7 +- .../lib/commands/HRANDFIELD_COUNT.spec.ts | 3 +- .../client/lib/commands/HRANDFIELD_COUNT.ts | 8 +- .../commands/HRANDFIELD_COUNT_WITHVALUES.ts | 8 +- packages/client/lib/commands/HSCAN.spec.ts | 9 +- packages/client/lib/commands/HSCAN.ts | 11 +- .../lib/commands/HSCAN_NOVALUES.spec.ts | 10 +- .../client/lib/commands/HSCAN_NOVALUES.ts | 18 +- packages/client/lib/commands/HSET.spec.ts | 15 +- packages/client/lib/commands/HSET.ts | 31 +- packages/client/lib/commands/HSETNX.spec.ts | 3 +- packages/client/lib/commands/HSETNX.ts | 9 +- packages/client/lib/commands/HSTRLEN.spec.ts | 3 +- packages/client/lib/commands/HSTRLEN.ts | 9 +- packages/client/lib/commands/HTTL.spec.ts | 6 +- packages/client/lib/commands/HTTL.ts | 15 +- packages/client/lib/commands/HVALS.spec.ts | 5 +- packages/client/lib/commands/HVALS.ts | 8 +- packages/client/lib/commands/INCR.spec.ts | 3 +- packages/client/lib/commands/INCR.ts | 7 +- packages/client/lib/commands/INCRBY.spec.ts | 3 +- packages/client/lib/commands/INCRBY.ts | 8 +- .../client/lib/commands/INCRBYFLOAT.spec.ts | 3 +- packages/client/lib/commands/INCRBYFLOAT.ts | 8 +- packages/client/lib/commands/INFO.spec.ts | 5 +- packages/client/lib/commands/INFO.ts | 11 +- packages/client/lib/commands/KEYS.ts | 7 +- packages/client/lib/commands/LASTSAVE.spec.ts | 3 +- packages/client/lib/commands/LASTSAVE.ts | 7 +- .../lib/commands/LATENCY_DOCTOR.spec.ts | 3 +- .../client/lib/commands/LATENCY_DOCTOR.ts | 7 +- .../client/lib/commands/LATENCY_GRAPH.spec.ts | 3 +- packages/client/lib/commands/LATENCY_GRAPH.ts | 7 +- .../lib/commands/LATENCY_HISTORY.spec.ts | 3 +- .../client/lib/commands/LATENCY_HISTORY.ts | 7 +- .../lib/commands/LATENCY_LATEST.spec.ts | 3 +- .../client/lib/commands/LATENCY_LATEST.ts | 7 +- packages/client/lib/commands/LCS.spec.ts | 3 +- packages/client/lib/commands/LCS.ts | 8 +- packages/client/lib/commands/LCS_IDX.spec.ts | 3 +- packages/client/lib/commands/LCS_IDX.ts | 13 +- .../lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts | 3 +- .../lib/commands/LCS_IDX_WITHMATCHLEN.ts | 17 +- packages/client/lib/commands/LCS_LEN.spec.ts | 3 +- packages/client/lib/commands/LCS_LEN.ts | 15 +- packages/client/lib/commands/LINDEX.spec.ts | 3 +- packages/client/lib/commands/LINDEX.ts | 9 +- packages/client/lib/commands/LINSERT.spec.ts | 3 +- packages/client/lib/commands/LINSERT.ts | 15 +- packages/client/lib/commands/LLEN.spec.ts | 3 +- packages/client/lib/commands/LLEN.ts | 8 +- packages/client/lib/commands/LMOVE.spec.ts | 3 +- packages/client/lib/commands/LMOVE.ts | 15 +- packages/client/lib/commands/LMPOP.spec.ts | 5 +- packages/client/lib/commands/LMPOP.ts | 30 +- packages/client/lib/commands/LOLWUT.spec.ts | 7 +- packages/client/lib/commands/LOLWUT.ts | 16 +- packages/client/lib/commands/LPOP.spec.ts | 3 +- packages/client/lib/commands/LPOP.ts | 7 +- .../client/lib/commands/LPOP_COUNT.spec.ts | 3 +- packages/client/lib/commands/LPOP_COUNT.ts | 8 +- packages/client/lib/commands/LPOS.spec.ts | 11 +- packages/client/lib/commands/LPOS.ts | 24 +- .../client/lib/commands/LPOS_COUNT.spec.ts | 15 +- packages/client/lib/commands/LPOS_COUNT.ts | 20 +- packages/client/lib/commands/LPUSH.spec.ts | 5 +- packages/client/lib/commands/LPUSH.ts | 10 +- packages/client/lib/commands/LPUSHX.spec.ts | 5 +- packages/client/lib/commands/LPUSHX.ts | 10 +- packages/client/lib/commands/LRANGE.spec.ts | 5 +- packages/client/lib/commands/LRANGE.ts | 18 +- packages/client/lib/commands/LREM.spec.ts | 3 +- packages/client/lib/commands/LREM.ts | 18 +- packages/client/lib/commands/LSET.spec.ts | 3 +- packages/client/lib/commands/LSET.ts | 17 +- packages/client/lib/commands/LTRIM.spec.ts | 3 +- packages/client/lib/commands/LTRIM.ts | 17 +- .../client/lib/commands/MEMORY_DOCTOR.spec.ts | 3 +- packages/client/lib/commands/MEMORY_DOCTOR.ts | 7 +- .../lib/commands/MEMORY_MALLOC-STATS.spec.ts | 3 +- .../lib/commands/MEMORY_MALLOC-STATS.ts | 7 +- .../client/lib/commands/MEMORY_PURGE.spec.ts | 3 +- packages/client/lib/commands/MEMORY_PURGE.ts | 7 +- .../client/lib/commands/MEMORY_STATS.spec.ts | 3 +- packages/client/lib/commands/MEMORY_STATS.ts | 7 +- .../client/lib/commands/MEMORY_USAGE.spec.ts | 5 +- packages/client/lib/commands/MEMORY_USAGE.ts | 11 +- packages/client/lib/commands/MGET.spec.ts | 5 +- packages/client/lib/commands/MGET.ts | 8 +- packages/client/lib/commands/MIGRATE.spec.ts | 15 +- packages/client/lib/commands/MIGRATE.ts | 30 +- .../client/lib/commands/MODULE_LIST.spec.ts | 3 +- packages/client/lib/commands/MODULE_LIST.ts | 7 +- .../client/lib/commands/MODULE_LOAD.spec.ts | 5 +- packages/client/lib/commands/MODULE_LOAD.ts | 11 +- .../client/lib/commands/MODULE_UNLOAD.spec.ts | 3 +- packages/client/lib/commands/MODULE_UNLOAD.ts | 7 +- packages/client/lib/commands/MOVE.spec.ts | 3 +- packages/client/lib/commands/MOVE.ts | 8 +- packages/client/lib/commands/MSET.spec.ts | 7 +- packages/client/lib/commands/MSET.ts | 32 +- packages/client/lib/commands/MSETNX.spec.ts | 7 +- packages/client/lib/commands/MSETNX.ts | 9 +- .../lib/commands/OBJECT_ENCODING.spec.ts | 3 +- .../client/lib/commands/OBJECT_ENCODING.ts | 7 +- .../client/lib/commands/OBJECT_FREQ.spec.ts | 3 +- packages/client/lib/commands/OBJECT_FREQ.ts | 7 +- .../lib/commands/OBJECT_IDLETIME.spec.ts | 3 +- .../client/lib/commands/OBJECT_IDLETIME.ts | 7 +- .../lib/commands/OBJECT_REFCOUNT.spec.ts | 3 +- .../client/lib/commands/OBJECT_REFCOUNT.ts | 7 +- packages/client/lib/commands/PERSIST.spec.ts | 3 +- packages/client/lib/commands/PERSIST.ts | 7 +- packages/client/lib/commands/PEXPIRE.spec.ts | 5 +- packages/client/lib/commands/PEXPIRE.ts | 13 +- .../client/lib/commands/PEXPIREAT.spec.ts | 7 +- packages/client/lib/commands/PEXPIREAT.ts | 13 +- .../client/lib/commands/PEXPIRETIME.spec.ts | 3 +- packages/client/lib/commands/PEXPIRETIME.ts | 7 +- packages/client/lib/commands/PFADD.spec.ts | 5 +- packages/client/lib/commands/PFADD.ts | 15 +- packages/client/lib/commands/PFCOUNT.spec.ts | 5 +- packages/client/lib/commands/PFCOUNT.ts | 9 +- packages/client/lib/commands/PFMERGE.spec.ts | 5 +- packages/client/lib/commands/PFMERGE.ts | 18 +- packages/client/lib/commands/PING.spec.ts | 5 +- packages/client/lib/commands/PING.ts | 11 +- packages/client/lib/commands/PSETEX.spec.ts | 3 +- packages/client/lib/commands/PSETEX.ts | 17 +- packages/client/lib/commands/PTTL.spec.ts | 3 +- packages/client/lib/commands/PTTL.ts | 7 +- packages/client/lib/commands/PUBLISH.spec.ts | 3 +- packages/client/lib/commands/PUBLISH.ts | 7 +- .../lib/commands/PUBSUB_CHANNELS.spec.ts | 5 +- .../client/lib/commands/PUBSUB_CHANNELS.ts | 11 +- .../client/lib/commands/PUBSUB_NUMPAT.spec.ts | 3 +- packages/client/lib/commands/PUBSUB_NUMPAT.ts | 7 +- .../client/lib/commands/PUBSUB_NUMSUB.spec.ts | 7 +- packages/client/lib/commands/PUBSUB_NUMSUB.ts | 15 +- .../lib/commands/PUBSUB_SHARDCHANNELS.spec.ts | 5 +- .../lib/commands/PUBSUB_SHARDCHANNELS.ts | 11 +- .../lib/commands/PUBSUB_SHARDNUMSUB.spec.ts | 7 +- .../client/lib/commands/PUBSUB_SHARDNUMSUB.ts | 14 +- .../client/lib/commands/RANDOMKEY.spec.ts | 3 +- packages/client/lib/commands/RANDOMKEY.ts | 7 +- packages/client/lib/commands/READONLY.spec.ts | 3 +- packages/client/lib/commands/READONLY.ts | 7 +- .../client/lib/commands/READWRITE.spec.ts | 3 +- packages/client/lib/commands/READWRITE.ts | 7 +- packages/client/lib/commands/RENAME.spec.ts | 3 +- packages/client/lib/commands/RENAME.ts | 7 +- packages/client/lib/commands/RENAMENX.spec.ts | 3 +- packages/client/lib/commands/RENAMENX.ts | 7 +- .../client/lib/commands/REPLICAOF.spec.ts | 3 +- packages/client/lib/commands/REPLICAOF.ts | 7 +- .../lib/commands/RESTORE-ASKING.spec.ts | 3 +- .../client/lib/commands/RESTORE-ASKING.ts | 7 +- packages/client/lib/commands/RESTORE.spec.ts | 13 +- packages/client/lib/commands/RESTORE.ts | 19 +- packages/client/lib/commands/ROLE.spec.ts | 3 +- packages/client/lib/commands/ROLE.ts | 7 +- packages/client/lib/commands/RPOP.spec.ts | 3 +- packages/client/lib/commands/RPOP.ts | 7 +- .../client/lib/commands/RPOPLPUSH.spec.ts | 3 +- packages/client/lib/commands/RPOPLPUSH.ts | 10 +- .../client/lib/commands/RPOP_COUNT.spec.ts | 3 +- packages/client/lib/commands/RPOP_COUNT.ts | 8 +- packages/client/lib/commands/RPUSH.spec.ts | 5 +- packages/client/lib/commands/RPUSH.ts | 13 +- packages/client/lib/commands/RPUSHX.spec.ts | 5 +- packages/client/lib/commands/RPUSHX.ts | 13 +- packages/client/lib/commands/SADD.spec.ts | 5 +- packages/client/lib/commands/SADD.ts | 10 +- packages/client/lib/commands/SAVE.spec.ts | 3 +- packages/client/lib/commands/SAVE.ts | 7 +- packages/client/lib/commands/SCAN.spec.ts | 11 +- packages/client/lib/commands/SCAN.ts | 27 +- packages/client/lib/commands/SCARD.spec.ts | 3 +- packages/client/lib/commands/SCARD.ts | 8 +- .../client/lib/commands/SCRIPT_DEBUG.spec.ts | 3 +- packages/client/lib/commands/SCRIPT_DEBUG.ts | 7 +- .../client/lib/commands/SCRIPT_EXISTS.spec.ts | 5 +- packages/client/lib/commands/SCRIPT_EXISTS.ts | 10 +- .../client/lib/commands/SCRIPT_FLUSH.spec.ts | 5 +- packages/client/lib/commands/SCRIPT_FLUSH.ts | 11 +- .../client/lib/commands/SCRIPT_KILL.spec.ts | 3 +- packages/client/lib/commands/SCRIPT_KILL.ts | 7 +- .../client/lib/commands/SCRIPT_LOAD.spec.ts | 3 +- packages/client/lib/commands/SCRIPT_LOAD.ts | 7 +- packages/client/lib/commands/SDIFF.spec.ts | 7 +- packages/client/lib/commands/SDIFF.ts | 10 +- .../client/lib/commands/SDIFFSTORE.spec.ts | 5 +- packages/client/lib/commands/SDIFFSTORE.ts | 10 +- packages/client/lib/commands/SET.spec.ts | 31 +- packages/client/lib/commands/SET.ts | 38 +- packages/client/lib/commands/SETBIT.spec.ts | 3 +- packages/client/lib/commands/SETBIT.ts | 12 +- packages/client/lib/commands/SETEX.spec.ts | 3 +- packages/client/lib/commands/SETEX.ts | 17 +- packages/client/lib/commands/SETNX .spec.ts | 3 +- packages/client/lib/commands/SETNX.ts | 8 +- packages/client/lib/commands/SETRANGE.spec.ts | 3 +- packages/client/lib/commands/SETRANGE.ts | 17 +- packages/client/lib/commands/SHUTDOWN.spec.ts | 11 +- packages/client/lib/commands/SHUTDOWN.ts | 17 +- packages/client/lib/commands/SINTER.spec.ts | 7 +- packages/client/lib/commands/SINTER.ts | 10 +- .../client/lib/commands/SINTERCARD.spec.ts | 7 +- packages/client/lib/commands/SINTERCARD.ts | 19 +- .../client/lib/commands/SINTERSTORE.spec.ts | 5 +- packages/client/lib/commands/SINTERSTORE.ts | 13 +- .../client/lib/commands/SISMEMBER.spec.ts | 5 +- packages/client/lib/commands/SISMEMBER.ts | 9 +- packages/client/lib/commands/SMEMBERS.spec.ts | 5 +- packages/client/lib/commands/SMEMBERS.ts | 8 +- .../client/lib/commands/SMISMEMBER.spec.ts | 5 +- packages/client/lib/commands/SMISMEMBER.ts | 9 +- packages/client/lib/commands/SMOVE.spec.ts | 3 +- packages/client/lib/commands/SMOVE.ts | 12 +- packages/client/lib/commands/SORT.spec.ts | 17 +- packages/client/lib/commands/SORT.ts | 27 +- packages/client/lib/commands/SORT_RO.spec.ts | 17 +- packages/client/lib/commands/SORT_RO.ts | 10 +- .../client/lib/commands/SORT_STORE.spec.ts | 17 +- packages/client/lib/commands/SORT_STORE.ts | 13 +- packages/client/lib/commands/SPOP.spec.ts | 3 +- packages/client/lib/commands/SPOP.ts | 7 +- .../client/lib/commands/SPOP_COUNT.spec.ts | 3 +- packages/client/lib/commands/SPOP_COUNT.ts | 8 +- packages/client/lib/commands/SPUBLISH.spec.ts | 3 +- packages/client/lib/commands/SPUBLISH.ts | 8 +- .../client/lib/commands/SRANDMEMBER.spec.ts | 3 +- packages/client/lib/commands/SRANDMEMBER.ts | 7 +- .../lib/commands/SRANDMEMBER_COUNT.spec.ts | 3 +- .../client/lib/commands/SRANDMEMBER_COUNT.ts | 9 +- packages/client/lib/commands/SREM.spec.ts | 5 +- packages/client/lib/commands/SREM.ts | 10 +- packages/client/lib/commands/SSCAN.spec.ts | 9 +- packages/client/lib/commands/SSCAN.ts | 11 +- packages/client/lib/commands/STRLEN.spec.ts | 3 +- packages/client/lib/commands/STRLEN.ts | 8 +- packages/client/lib/commands/SUNION.spec.ts | 5 +- packages/client/lib/commands/SUNION.ts | 10 +- .../client/lib/commands/SUNIONSTORE.spec.ts | 5 +- packages/client/lib/commands/SUNIONSTORE.ts | 13 +- packages/client/lib/commands/SWAPDB.spec.ts | 3 +- packages/client/lib/commands/SWAPDB.ts | 7 +- packages/client/lib/commands/TIME.spec.ts | 3 +- packages/client/lib/commands/TIME.ts | 7 +- packages/client/lib/commands/TOUCH.spec.ts | 5 +- packages/client/lib/commands/TOUCH.ts | 9 +- packages/client/lib/commands/TTL.spec.ts | 3 +- packages/client/lib/commands/TTL.ts | 7 +- packages/client/lib/commands/TYPE.spec.ts | 5 +- packages/client/lib/commands/TYPE.ts | 8 +- packages/client/lib/commands/UNLINK.spec.ts | 5 +- packages/client/lib/commands/UNLINK.ts | 9 +- packages/client/lib/commands/WAIT.spec.ts | 3 +- packages/client/lib/commands/WAIT.ts | 7 +- packages/client/lib/commands/XACK.spec.ts | 5 +- packages/client/lib/commands/XACK.ts | 16 +- packages/client/lib/commands/XADD.spec.ts | 13 +- packages/client/lib/commands/XADD.ts | 40 +- .../lib/commands/XADD_NOMKSTREAM.spec.ts | 13 +- .../client/lib/commands/XADD_NOMKSTREAM.ts | 15 +- .../client/lib/commands/XAUTOCLAIM.spec.ts | 5 +- packages/client/lib/commands/XAUTOCLAIM.ts | 20 +- .../lib/commands/XAUTOCLAIM_JUSTID.spec.ts | 3 +- .../client/lib/commands/XAUTOCLAIM_JUSTID.ts | 9 +- packages/client/lib/commands/XCLAIM.spec.ts | 19 +- packages/client/lib/commands/XCLAIM.ts | 27 +- .../client/lib/commands/XCLAIM_JUSTID.spec.ts | 3 +- packages/client/lib/commands/XCLAIM_JUSTID.ts | 9 +- packages/client/lib/commands/XDEL.spec.ts | 5 +- packages/client/lib/commands/XDEL.ts | 10 +- .../client/lib/commands/XGROUP_CREATE.spec.ts | 7 +- packages/client/lib/commands/XGROUP_CREATE.ts | 15 +- .../commands/XGROUP_CREATECONSUMER.spec.ts | 3 +- .../lib/commands/XGROUP_CREATECONSUMER.ts | 9 +- .../lib/commands/XGROUP_DELCONSUMER.spec.ts | 3 +- .../client/lib/commands/XGROUP_DELCONSUMER.ts | 9 +- .../lib/commands/XGROUP_DESTROY.spec.ts | 3 +- .../client/lib/commands/XGROUP_DESTROY.ts | 11 +- .../client/lib/commands/XGROUP_SETID.spec.ts | 3 +- packages/client/lib/commands/XGROUP_SETID.ts | 13 +- .../lib/commands/XINFO_CONSUMERS.spec.ts | 3 +- .../client/lib/commands/XINFO_CONSUMERS.ts | 11 +- .../client/lib/commands/XINFO_GROUPS.spec.ts | 3 +- packages/client/lib/commands/XINFO_GROUPS.ts | 7 +- .../client/lib/commands/XINFO_STREAM.spec.ts | 3 +- packages/client/lib/commands/XINFO_STREAM.ts | 7 +- packages/client/lib/commands/XLEN.spec.ts | 5 +- packages/client/lib/commands/XLEN.ts | 8 +- packages/client/lib/commands/XPENDING.spec.ts | 3 +- packages/client/lib/commands/XPENDING.ts | 9 +- .../lib/commands/XPENDING_RANGE.spec.ts | 9 +- .../client/lib/commands/XPENDING_RANGE.ts | 24 +- packages/client/lib/commands/XRANGE.spec.ts | 5 +- packages/client/lib/commands/XRANGE.ts | 15 +- packages/client/lib/commands/XREAD.spec.ts | 18 +- packages/client/lib/commands/XREAD.ts | 33 +- .../client/lib/commands/XREADGROUP.spec.ts | 19 +- packages/client/lib/commands/XREADGROUP.ts | 25 +- .../client/lib/commands/XREVRANGE.spec.ts | 5 +- packages/client/lib/commands/XREVRANGE.ts | 13 +- packages/client/lib/commands/XSETID.spec.ts | 7 +- packages/client/lib/commands/XSETID.ts | 15 +- packages/client/lib/commands/XTRIM.spec.ts | 9 +- packages/client/lib/commands/XTRIM.ts | 17 +- packages/client/lib/commands/ZADD.spec.ts | 21 +- packages/client/lib/commands/ZADD.ts | 36 +- .../client/lib/commands/ZADD_INCR.spec.ts | 13 +- packages/client/lib/commands/ZADD_INCR.ts | 20 +- packages/client/lib/commands/ZCARD.spec.ts | 3 +- packages/client/lib/commands/ZCARD.ts | 8 +- packages/client/lib/commands/ZCOUNT.spec.ts | 3 +- packages/client/lib/commands/ZCOUNT.ts | 16 +- packages/client/lib/commands/ZDIFF.spec.ts | 5 +- packages/client/lib/commands/ZDIFF.ts | 9 +- .../client/lib/commands/ZDIFFSTORE.spec.ts | 5 +- packages/client/lib/commands/ZDIFFSTORE.ts | 13 +- .../lib/commands/ZDIFF_WITHSCORES.spec.ts | 5 +- .../client/lib/commands/ZDIFF_WITHSCORES.ts | 12 +- packages/client/lib/commands/ZINCRBY.spec.ts | 3 +- packages/client/lib/commands/ZINCRBY.ts | 14 +- packages/client/lib/commands/ZINTER.spec.ts | 11 +- packages/client/lib/commands/ZINTER.ts | 24 +- .../client/lib/commands/ZINTERCARD.spec.ts | 7 +- packages/client/lib/commands/ZINTERCARD.ts | 16 +- .../client/lib/commands/ZINTERSTORE.spec.ts | 11 +- packages/client/lib/commands/ZINTERSTORE.ts | 11 +- .../lib/commands/ZINTER_WITHSCORES.spec.ts | 11 +- .../client/lib/commands/ZINTER_WITHSCORES.ts | 11 +- .../client/lib/commands/ZLEXCOUNT.spec.ts | 3 +- packages/client/lib/commands/ZLEXCOUNT.ts | 16 +- packages/client/lib/commands/ZMPOP.spec.ts | 5 +- packages/client/lib/commands/ZMPOP.ts | 30 +- packages/client/lib/commands/ZMSCORE.spec.ts | 5 +- packages/client/lib/commands/ZMSCORE.ts | 14 +- packages/client/lib/commands/ZPOPMAX.spec.ts | 3 +- packages/client/lib/commands/ZPOPMAX.ts | 7 +- .../client/lib/commands/ZPOPMAX_COUNT.spec.ts | 3 +- packages/client/lib/commands/ZPOPMAX_COUNT.ts | 8 +- packages/client/lib/commands/ZPOPMIN.spec.ts | 3 +- packages/client/lib/commands/ZPOPMIN.ts | 7 +- .../client/lib/commands/ZPOPMIN_COUNT.spec.ts | 3 +- packages/client/lib/commands/ZPOPMIN_COUNT.ts | 8 +- .../client/lib/commands/ZRANDMEMBER.spec.ts | 3 +- packages/client/lib/commands/ZRANDMEMBER.ts | 7 +- .../lib/commands/ZRANDMEMBER_COUNT.spec.ts | 3 +- .../client/lib/commands/ZRANDMEMBER_COUNT.ts | 9 +- .../ZRANDMEMBER_COUNT_WITHSCORES.spec.ts | 3 +- .../commands/ZRANDMEMBER_COUNT_WITHSCORES.ts | 11 +- packages/client/lib/commands/ZRANGE.spec.ts | 13 +- packages/client/lib/commands/ZRANGE.ts | 74 +- .../client/lib/commands/ZRANGEBYLEX.spec.ts | 5 +- packages/client/lib/commands/ZRANGEBYLEX.ts | 18 +- .../client/lib/commands/ZRANGEBYSCORE.spec.ts | 5 +- packages/client/lib/commands/ZRANGEBYSCORE.ts | 18 +- .../commands/ZRANGEBYSCORE_WITHSCORES.spec.ts | 5 +- .../lib/commands/ZRANGEBYSCORE_WITHSCORES.ts | 13 +- .../client/lib/commands/ZRANGESTORE.spec.ts | 13 +- packages/client/lib/commands/ZRANGESTORE.ts | 27 +- .../lib/commands/ZRANGE_WITHSCORES.spec.ts | 11 +- .../client/lib/commands/ZRANGE_WITHSCORES.ts | 13 +- packages/client/lib/commands/ZRANK.spec.ts | 3 +- packages/client/lib/commands/ZRANK.ts | 9 +- .../lib/commands/ZRANK_WITHSCORE.spec.ts | 3 +- .../client/lib/commands/ZRANK_WITHSCORE.ts | 11 +- packages/client/lib/commands/ZREM.spec.ts | 5 +- packages/client/lib/commands/ZREM.ts | 11 +- .../lib/commands/ZREMRANGEBYLEX.spec.ts | 3 +- .../client/lib/commands/ZREMRANGEBYLEX.ts | 13 +- .../lib/commands/ZREMRANGEBYRANK.spec.ts | 3 +- .../client/lib/commands/ZREMRANGEBYRANK.ts | 15 +- .../lib/commands/ZREMRANGEBYSCORE.spec.ts | 3 +- .../client/lib/commands/ZREMRANGEBYSCORE.ts | 13 +- packages/client/lib/commands/ZREVRANK.spec.ts | 3 +- packages/client/lib/commands/ZREVRANK.ts | 9 +- packages/client/lib/commands/ZSCAN.spec.ts | 9 +- packages/client/lib/commands/ZSCAN.ts | 11 +- packages/client/lib/commands/ZSCORE.spec.ts | 3 +- packages/client/lib/commands/ZSCORE.ts | 9 +- packages/client/lib/commands/ZUNION.spec.ts | 11 +- packages/client/lib/commands/ZUNION.ts | 16 +- .../client/lib/commands/ZUNIONSTORE.spec.ts | 11 +- packages/client/lib/commands/ZUNIONSTORE.ts | 19 +- .../lib/commands/ZUNION_WITHSCORES.spec.ts | 11 +- .../client/lib/commands/ZUNION_WITHSCORES.ts | 13 +- .../lib/commands/generic-transformers.ts | 93 +- packages/client/lib/multi-command.ts | 10 +- .../lib/sentinel/commands/SENTINEL_MASTER.ts | 5 +- .../lib/sentinel/commands/SENTINEL_MONITOR.ts | 5 +- .../sentinel/commands/SENTINEL_REPLICAS.ts | 5 +- .../sentinel/commands/SENTINEL_SENTINELS.ts | 5 +- .../lib/sentinel/commands/SENTINEL_SET.ts | 9 +- packages/client/lib/sentinel/index.spec.ts | 13 +- packages/client/lib/sentinel/index.ts | 26 +- .../client/lib/sentinel/multi-commands.ts | 107 +- packages/client/lib/sentinel/utils.ts | 70 +- packages/client/lib/test-utils.ts | 8 + .../graph/lib/commands/CONFIG_GET.spec.ts | 3 +- packages/graph/lib/commands/CONFIG_GET.ts | 9 +- .../graph/lib/commands/CONFIG_SET.spec.ts | 3 +- packages/graph/lib/commands/CONFIG_SET.ts | 14 +- packages/graph/lib/commands/DELETE.spec.ts | 3 +- packages/graph/lib/commands/DELETE.ts | 9 +- packages/graph/lib/commands/EXPLAIN.spec.ts | 3 +- packages/graph/lib/commands/EXPLAIN.ts | 10 +- packages/graph/lib/commands/LIST.spec.ts | 3 +- packages/graph/lib/commands/LIST.ts | 9 +- packages/graph/lib/commands/PROFILE.spec.ts | 3 +- packages/graph/lib/commands/PROFILE.ts | 10 +- packages/graph/lib/commands/QUERY.spec.ts | 11 +- packages/graph/lib/commands/QUERY.ts | 28 +- packages/graph/lib/commands/RO_QUERY.spec.ts | 3 +- packages/graph/lib/commands/RO_QUERY.ts | 7 +- packages/graph/lib/commands/SLOWLOG.spec.ts | 3 +- packages/graph/lib/commands/SLOWLOG.ts | 9 +- packages/graph/lib/commands/index.ts | 2 +- packages/graph/lib/graph.ts | 2 +- packages/json/lib/commands/ARRAPPEND.spec.ts | 5 +- packages/json/lib/commands/ARRAPPEND.ts | 20 +- packages/json/lib/commands/ARRINDEX.spec.ts | 7 +- packages/json/lib/commands/ARRINDEX.ts | 17 +- packages/json/lib/commands/ARRINSERT.spec.ts | 5 +- packages/json/lib/commands/ARRINSERT.ts | 21 +- packages/json/lib/commands/ARRLEN.spec.ts | 5 +- packages/json/lib/commands/ARRLEN.ts | 14 +- packages/json/lib/commands/ARRPOP.spec.ts | 7 +- packages/json/lib/commands/ARRPOP.ts | 17 +- packages/json/lib/commands/ARRTRIM.spec.ts | 3 +- packages/json/lib/commands/ARRTRIM.ts | 10 +- packages/json/lib/commands/CLEAR.spec.ts | 5 +- packages/json/lib/commands/CLEAR.ts | 13 +- .../json/lib/commands/DEBUG_MEMORY.spec.ts | 5 +- packages/json/lib/commands/DEBUG_MEMORY.ts | 13 +- packages/json/lib/commands/DEL.spec.ts | 5 +- packages/json/lib/commands/DEL.ts | 13 +- packages/json/lib/commands/FORGET.spec.ts | 5 +- packages/json/lib/commands/FORGET.ts | 13 +- packages/json/lib/commands/GET.spec.ts | 7 +- packages/json/lib/commands/GET.ts | 16 +- packages/json/lib/commands/MERGE.spec.ts | 3 +- packages/json/lib/commands/MERGE.ts | 15 +- packages/json/lib/commands/MGET.spec.ts | 3 +- packages/json/lib/commands/MGET.ts | 14 +- packages/json/lib/commands/MSET.spec.ts | 3 +- packages/json/lib/commands/MSET.ts | 18 +- packages/json/lib/commands/NUMINCRBY.spec.ts | 3 +- packages/json/lib/commands/NUMINCRBY.ts | 10 +- packages/json/lib/commands/NUMMULTBY.spec.ts | 3 +- packages/json/lib/commands/NUMMULTBY.ts | 10 +- packages/json/lib/commands/OBJKEYS.spec.ts | 5 +- packages/json/lib/commands/OBJKEYS.ts | 14 +- packages/json/lib/commands/OBJLEN.spec.ts | 5 +- packages/json/lib/commands/OBJLEN.ts | 14 +- packages/json/lib/commands/RESP.spec.ts | 7 +- packages/json/lib/commands/RESP.ts | 25 +- packages/json/lib/commands/SET.spec.ts | 7 +- packages/json/lib/commands/SET.ts | 19 +- packages/json/lib/commands/STRAPPEND.spec.ts | 5 +- packages/json/lib/commands/STRAPPEND.ts | 14 +- packages/json/lib/commands/STRLEN.spec.ts | 5 +- packages/json/lib/commands/STRLEN.ts | 13 +- packages/json/lib/commands/TOGGLE.spec.ts | 3 +- packages/json/lib/commands/TOGGLE.ts | 10 +- packages/json/lib/commands/TYPE.spec.ts | 5 +- packages/json/lib/commands/TYPE.ts | 14 +- packages/json/lib/commands/index.ts | 4 +- .../search/lib/commands/AGGREGATE.spec.ts | 70 +- packages/search/lib/commands/AGGREGATE.ts | 73 +- .../lib/commands/AGGREGATE_WITHCURSOR.spec.ts | 7 +- .../lib/commands/AGGREGATE_WITHCURSOR.ts | 17 +- packages/search/lib/commands/ALIASADD.spec.ts | 3 +- packages/search/lib/commands/ALIASADD.ts | 9 +- packages/search/lib/commands/ALIASDEL.spec.ts | 3 +- packages/search/lib/commands/ALIASDEL.ts | 9 +- .../search/lib/commands/ALIASUPDATE.spec.ts | 3 +- packages/search/lib/commands/ALIASUPDATE.ts | 9 +- packages/search/lib/commands/ALTER.spec.ts | 3 +- packages/search/lib/commands/ALTER.ts | 14 +- .../search/lib/commands/CONFIG_GET.spec.ts | 3 +- packages/search/lib/commands/CONFIG_GET.ts | 9 +- .../search/lib/commands/CONFIG_SET.spec.ts | 3 +- packages/search/lib/commands/CONFIG_SET.ts | 9 +- packages/search/lib/commands/CREATE.spec.ts | 83 +- packages/search/lib/commands/CREATE.ts | 103 +- .../search/lib/commands/CURSOR_DEL.spec.ts | 3 +- packages/search/lib/commands/CURSOR_DEL.ts | 9 +- .../search/lib/commands/CURSOR_READ.spec.ts | 5 +- packages/search/lib/commands/CURSOR_READ.ts | 13 +- packages/search/lib/commands/DICTADD.spec.ts | 5 +- packages/search/lib/commands/DICTADD.ts | 12 +- packages/search/lib/commands/DICTDEL.spec.ts | 5 +- packages/search/lib/commands/DICTDEL.ts | 12 +- packages/search/lib/commands/DICTDUMP.spec.ts | 3 +- packages/search/lib/commands/DICTDUMP.ts | 9 +- .../search/lib/commands/DROPINDEX.spec.ts | 5 +- packages/search/lib/commands/DROPINDEX.ts | 13 +- packages/search/lib/commands/EXPLAIN.spec.ts | 7 +- packages/search/lib/commands/EXPLAIN.ts | 18 +- .../search/lib/commands/EXPLAINCLI.spec.ts | 3 +- packages/search/lib/commands/EXPLAINCLI.ts | 9 +- packages/search/lib/commands/INFO.spec.ts | 3 +- packages/search/lib/commands/INFO.ts | 11 +- .../lib/commands/PROFILE_AGGREGATE.spec.ts | 5 +- .../search/lib/commands/PROFILE_AGGREGATE.ts | 21 +- .../lib/commands/PROFILE_SEARCH.spec.ts | 5 +- .../search/lib/commands/PROFILE_SEARCH.ts | 22 +- packages/search/lib/commands/SEARCH.spec.ts | 53 +- packages/search/lib/commands/SEARCH.ts | 77 +- .../lib/commands/SEARCH_NOCONTENT.spec.ts | 3 +- .../search/lib/commands/SEARCH_NOCONTENT.ts | 11 +- .../search/lib/commands/SPELLCHECK.spec.ts | 11 +- packages/search/lib/commands/SPELLCHECK.ts | 27 +- packages/search/lib/commands/SUGADD.spec.ts | 7 +- packages/search/lib/commands/SUGADD.ts | 16 +- packages/search/lib/commands/SUGDEL.spec.ts | 3 +- packages/search/lib/commands/SUGDEL.ts | 10 +- packages/search/lib/commands/SUGGET.spec.ts | 7 +- packages/search/lib/commands/SUGGET.ts | 16 +- .../lib/commands/SUGGET_WITHPAYLOADS.spec.ts | 3 +- .../lib/commands/SUGGET_WITHPAYLOADS.ts | 12 +- .../lib/commands/SUGGET_WITHSCORES.spec.ts | 3 +- .../search/lib/commands/SUGGET_WITHSCORES.ts | 12 +- .../SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts | 3 +- .../SUGGET_WITHSCORES_WITHPAYLOADS.ts | 12 +- packages/search/lib/commands/SUGLEN.spec.ts | 3 +- packages/search/lib/commands/SUGLEN.ts | 8 +- packages/search/lib/commands/SYNDUMP.spec.ts | 3 +- packages/search/lib/commands/SYNDUMP.ts | 9 +- .../search/lib/commands/SYNUPDATE.spec.ts | 7 +- packages/search/lib/commands/SYNUPDATE.ts | 16 +- packages/search/lib/commands/TAGVALS.spec.ts | 3 +- packages/search/lib/commands/TAGVALS.ts | 9 +- packages/search/lib/commands/_LIST.spec.ts | 3 +- packages/search/lib/commands/_LIST.ts | 9 +- packages/test-utils/lib/dockers.ts | 2 +- packages/time-series/lib/commands/ADD.spec.ts | 17 +- packages/time-series/lib/commands/ADD.ts | 40 +- .../time-series/lib/commands/ALTER.spec.ts | 15 +- packages/time-series/lib/commands/ALTER.ts | 25 +- .../time-series/lib/commands/CREATE.spec.ts | 17 +- packages/time-series/lib/commands/CREATE.ts | 35 +- .../lib/commands/CREATERULE.spec.ts | 5 +- .../time-series/lib/commands/CREATERULE.ts | 22 +- .../time-series/lib/commands/DECRBY.spec.ts | 17 +- packages/time-series/lib/commands/DECRBY.ts | 12 +- packages/time-series/lib/commands/DEL.spec.ts | 3 +- packages/time-series/lib/commands/DEL.ts | 15 +- .../lib/commands/DELETERULE.spec.ts | 3 +- .../time-series/lib/commands/DELETERULE.ts | 13 +- packages/time-series/lib/commands/GET.spec.ts | 5 +- packages/time-series/lib/commands/GET.ts | 13 +- .../time-series/lib/commands/INCRBY.spec.ts | 19 +- packages/time-series/lib/commands/INCRBY.ts | 38 +- .../time-series/lib/commands/INFO.spec.ts | 3 +- packages/time-series/lib/commands/INFO.ts | 9 +- .../lib/commands/INFO_DEBUG.spec.ts | 3 +- .../time-series/lib/commands/INFO_DEBUG.ts | 14 +- .../time-series/lib/commands/MADD.spec.ts | 3 +- packages/time-series/lib/commands/MADD.ts | 17 +- .../time-series/lib/commands/MGET.spec.ts | 5 +- packages/time-series/lib/commands/MGET.ts | 26 +- .../lib/commands/MGET_SELECTED_LABELS.spec.ts | 3 +- .../lib/commands/MGET_SELECTED_LABELS.ts | 19 +- .../lib/commands/MGET_WITHLABELS.spec.ts | 3 +- .../lib/commands/MGET_WITHLABELS.ts | 17 +- .../time-series/lib/commands/MRANGE.spec.ts | 3 +- packages/time-series/lib/commands/MRANGE.ts | 21 +- .../lib/commands/MRANGE_GROUPBY.spec.ts | 3 +- .../lib/commands/MRANGE_GROUPBY.ts | 33 +- .../commands/MRANGE_SELECTED_LABELS.spec.ts | 3 +- .../lib/commands/MRANGE_SELECTED_LABELS.ts | 24 +- .../MRANGE_SELECTED_LABELS_GROUPBY.spec.ts | 3 +- .../MRANGE_SELECTED_LABELS_GROUPBY.ts | 30 +- .../lib/commands/MRANGE_WITHLABELS.spec.ts | 3 +- .../lib/commands/MRANGE_WITHLABELS.ts | 23 +- .../MRANGE_WITHLABELS_GROUPBY.spec.ts | 3 +- .../lib/commands/MRANGE_WITHLABELS_GROUPBY.ts | 30 +- .../lib/commands/MREVRANGE.spec.ts | 3 +- .../time-series/lib/commands/MREVRANGE.ts | 6 +- .../lib/commands/MREVRANGE_GROUPBY.spec.ts | 3 +- .../lib/commands/MREVRANGE_GROUPBY.ts | 5 +- .../MREVRANGE_SELECTED_LABELS.spec.ts | 3 +- .../lib/commands/MREVRANGE_SELECTED_LABELS.ts | 5 +- .../MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts | 3 +- .../MREVRANGE_SELECTED_LABELS_GROUPBY.ts | 5 +- .../lib/commands/MREVRANGE_WITHLABELS.spec.ts | 3 +- .../lib/commands/MREVRANGE_WITHLABELS.ts | 6 +- .../MREVRANGE_WITHLABELS_GROUPBY.spec.ts | 3 +- .../commands/MREVRANGE_WITHLABELS_GROUPBY.ts | 5 +- .../lib/commands/QUERYINDEX.spec.ts | 5 +- .../time-series/lib/commands/QUERYINDEX.ts | 12 +- .../time-series/lib/commands/RANGE.spec.ts | 3 +- packages/time-series/lib/commands/RANGE.ts | 49 +- .../time-series/lib/commands/REVRANGE.spec.ts | 3 +- packages/time-series/lib/commands/REVRANGE.ts | 10 +- .../time-series/lib/commands/index.spec.ts | 2 +- packages/time-series/lib/commands/index.ts | 41 +- 1016 files changed, 6345 insertions(+), 6540 deletions(-) create mode 100644 packages/client/lib/client/parser.ts diff --git a/package-lock.json b/package-lock.json index aefd0678434..ba18a98b6a5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,9 +23,8 @@ }, "node_modules/@ampproject/remapping": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -36,9 +35,8 @@ }, "node_modules/@babel/code-frame": { "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/highlight": "^7.23.4", "chalk": "^2.4.2" @@ -49,9 +47,8 @@ }, "node_modules/@babel/code-frame/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, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -61,9 +58,8 @@ }, "node_modules/@babel/code-frame/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, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -75,42 +71,37 @@ }, "node_modules/@babel/code-frame/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, + "license": "MIT", "dependencies": { "color-name": "1.1.3" } }, "node_modules/@babel/code-frame/node_modules/color-name": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@babel/code-frame/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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/@babel/code-frame/node_modules/has-flag": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/@babel/code-frame/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, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -120,18 +111,16 @@ }, "node_modules/@babel/compat-data": { "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.23.5.tgz", - "integrity": "sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.23.9.tgz", - "integrity": "sha512-5q0175NOjddqpvvzU+kDiSOAk4PfdO6FvwCWoQ6RO7rTzEe8vlo+4HVfcnAREhD4npMs0e9uZypjTwzZPCf/cw==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.23.5", @@ -159,15 +148,13 @@ }, "node_modules/@babel/core/node_modules/convert-source-map": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@babel/generator": { "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", - "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", @@ -180,9 +167,8 @@ }, "node_modules/@babel/helper-compilation-targets": { "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", - "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/compat-data": "^7.23.5", "@babel/helper-validator-option": "^7.23.5", @@ -196,18 +182,16 @@ }, "node_modules/@babel/helper-environment-visitor": { "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", - "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { "version": "7.23.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", - "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.22.15", "@babel/types": "^7.23.0" @@ -218,9 +202,8 @@ }, "node_modules/@babel/helper-hoist-variables": { "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", - "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" }, @@ -230,9 +213,8 @@ }, "node_modules/@babel/helper-module-imports": { "version": "7.22.15", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", - "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.22.15" }, @@ -242,9 +224,8 @@ }, "node_modules/@babel/helper-module-transforms": { "version": "7.23.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz", - "integrity": "sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-environment-visitor": "^7.22.20", "@babel/helper-module-imports": "^7.22.15", @@ -261,9 +242,8 @@ }, "node_modules/@babel/helper-simple-access": { "version": "7.22.5", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", - "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" }, @@ -273,9 +253,8 @@ }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.22.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", - "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.22.5" }, @@ -285,36 +264,32 @@ }, "node_modules/@babel/helper-string-parser": { "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", - "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.23.9.tgz", - "integrity": "sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/template": "^7.23.9", "@babel/traverse": "^7.23.9", @@ -326,9 +301,8 @@ }, "node_modules/@babel/highlight": { "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", @@ -340,9 +314,8 @@ }, "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, + "license": "MIT", "dependencies": { "color-convert": "^1.9.0" }, @@ -352,9 +325,8 @@ }, "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, + "license": "MIT", "dependencies": { "ansi-styles": "^3.2.1", "escape-string-regexp": "^1.0.5", @@ -366,42 +338,37 @@ }, "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, + "license": "MIT", "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": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "dev": true, + "license": "MIT" }, "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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "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": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", "dev": true, + "license": "MIT", "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, + "license": "MIT", "dependencies": { "has-flag": "^3.0.0" }, @@ -411,9 +378,8 @@ }, "node_modules/@babel/parser": { "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", "dev": true, + "license": "MIT", "bin": { "parser": "bin/babel-parser.js" }, @@ -423,9 +389,8 @@ }, "node_modules/@babel/template": { "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz", - "integrity": "sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.23.5", "@babel/parser": "^7.23.9", @@ -437,9 +402,8 @@ }, "node_modules/@babel/traverse": { "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.9.tgz", - "integrity": "sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.23.5", "@babel/generator": "^7.23.6", @@ -458,9 +422,8 @@ }, "node_modules/@babel/types": { "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", "dev": true, + "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", @@ -470,270 +433,13 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -742,113 +448,15 @@ "node": ">=12" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "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 + "dev": true, + "license": "ISC" }, "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, + "license": "ISC", "dependencies": { "camelcase": "^5.3.1", "find-up": "^4.1.0", @@ -862,18 +470,16 @@ }, "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, + "license": "MIT", "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, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -884,9 +490,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, + "license": "MIT", "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -897,9 +502,8 @@ }, "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, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -909,9 +513,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, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -924,9 +527,8 @@ }, "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, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -936,9 +538,8 @@ }, "node_modules/@istanbuljs/nyc-config-typescript": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@istanbuljs/nyc-config-typescript/-/nyc-config-typescript-1.0.2.tgz", - "integrity": "sha512-iKGIyMoyJuFnJRSVTZ78POIRvNnwZaWIf8vG4ZS3rQq58MMDrqEX2nnzx0R28V2X8JvmKYiqY9FP2hlJsm8A0w==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2" }, @@ -951,18 +552,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.0.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -974,33 +573,29 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" @@ -1008,9 +603,8 @@ }, "node_modules/@ljharb/through": { "version": "2.3.12", - "resolved": "https://registry.npmjs.org/@ljharb/through/-/through-2.3.12.tgz", - "integrity": "sha512-ajo/heTlG3QgC8EGP6APIejksVAYt4ayz4tqoP3MolFELzcH1x1fzwEYRJTPO0IELutZ5HQ0c26/GqAYy79u3g==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.5" }, @@ -1020,9 +614,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, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" @@ -1033,18 +626,16 @@ }, "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, + "license": "MIT", "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, + "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" @@ -1055,18 +646,16 @@ }, "node_modules/@octokit/auth-token": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-4.0.0.tgz", - "integrity": "sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 18" } }, "node_modules/@octokit/core": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.1.0.tgz", - "integrity": "sha512-BDa2VAMLSh3otEiaMJ/3Y36GU4qf6GI+VivQ/P41NC6GHcdxpKlqV0ikSZ5gdQsmS3ojXeRx5vasgNTinF0Q4g==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.0.0", @@ -1082,9 +671,8 @@ }, "node_modules/@octokit/endpoint": { "version": "9.0.4", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-9.0.4.tgz", - "integrity": "sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^12.0.0", "universal-user-agent": "^6.0.0" @@ -1095,9 +683,8 @@ }, "node_modules/@octokit/graphql": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-7.0.2.tgz", - "integrity": "sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/request": "^8.0.1", "@octokit/types": "^12.0.0", @@ -1109,15 +696,13 @@ }, "node_modules/@octokit/openapi-types": { "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-19.1.0.tgz", - "integrity": "sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@octokit/plugin-paginate-rest": { "version": "9.1.5", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-9.1.5.tgz", - "integrity": "sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^12.4.0" }, @@ -1130,9 +715,8 @@ }, "node_modules/@octokit/plugin-request-log": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-4.0.0.tgz", - "integrity": "sha512-2uJI1COtYCq8Z4yNSnM231TgH50bRkheQ9+aH8TnZanB6QilOnx8RMD2qsnamSOXtDj0ilxvevf5fGsBhBBzKA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 18" }, @@ -1142,9 +726,8 @@ }, "node_modules/@octokit/plugin-rest-endpoint-methods": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-10.2.0.tgz", - "integrity": "sha512-ePbgBMYtGoRNXDyKGvr9cyHjQ163PbwD0y1MkDJCpkO2YH4OeXX40c4wYHKikHGZcpGPbcRLuy0unPUuafco8Q==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^12.3.0" }, @@ -1157,9 +740,8 @@ }, "node_modules/@octokit/request": { "version": "8.1.6", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-8.1.6.tgz", - "integrity": "sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/endpoint": "^9.0.0", "@octokit/request-error": "^5.0.0", @@ -1172,9 +754,8 @@ }, "node_modules/@octokit/request-error": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-5.0.1.tgz", - "integrity": "sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/types": "^12.0.0", "deprecation": "^2.0.0", @@ -1186,9 +767,8 @@ }, "node_modules/@octokit/rest": { "version": "20.0.2", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-20.0.2.tgz", - "integrity": "sha512-Ux8NDgEraQ/DMAU1PlAohyfBBXDwhnX2j33Z1nJNziqAfHi70PuxkFYIcIt8aIAxtRE7KVuKp8lSR8pA0J5iOQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/core": "^5.0.0", "@octokit/plugin-paginate-rest": "^9.0.0", @@ -1201,27 +781,24 @@ }, "node_modules/@octokit/types": { "version": "12.4.0", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-12.4.0.tgz", - "integrity": "sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==", "dev": true, + "license": "MIT", "dependencies": { "@octokit/openapi-types": "^19.1.0" } }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.22.0" } }, "node_modules/@pnpm/network.ca-file": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "4.2.10" }, @@ -1231,15 +808,13 @@ }, "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/@pnpm/npm-conf": { "version": "2.2.2", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.2.2.tgz", - "integrity": "sha512-UA91GwWPhFExt3IizW6bOeY/pQ0BkuNwKjk9iQW9KqxluGCrg4VenZ0/L+2Y0+ZOtme72EVvg6v0zo3AMQRCeA==", "dev": true, + "license": "MIT", "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", @@ -1279,9 +854,8 @@ }, "node_modules/@sindresorhus/is": { "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -1291,9 +865,8 @@ }, "node_modules/@sindresorhus/merge-streams": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-1.0.0.tgz", - "integrity": "sha512-rUV5WyJrJLoloD4NDN1V1+LDMDWOa4OTsT4yYJwQNpTU6FWxkxHpL7eu4w+DmiH8x/EAM1otkPE1+LaspIbplw==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -1303,27 +876,24 @@ }, "node_modules/@sinonjs/commons": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/fake-timers": { "version": "11.2.2", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.2.2.tgz", - "integrity": "sha512-G2piCSxQ7oWOxwGSAyFHfPIsyeJGXYtc6mFbnFA+kRXkiEnTl8c/8jul2S329iFBnDI9HGoeWWAZvuvOkZccgw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "node_modules/@sinonjs/samsam": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", - "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^2.0.0", "lodash.get": "^4.4.2", @@ -1332,24 +902,21 @@ }, "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", - "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "type-detect": "4.0.8" } }, "node_modules/@sinonjs/text-encoding": { "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", - "dev": true + "dev": true, + "license": "(Unlicense OR Apache-2.0)" }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", "dev": true, + "license": "MIT", "dependencies": { "defer-to-connect": "^2.0.1" }, @@ -1359,66 +926,57 @@ }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", - "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/mocha": { "version": "10.0.6", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.6.tgz", - "integrity": "sha512-dJvrYWxP/UcXm36Qn36fxhUKu8A/xMRXVT2cliFF1Z7UA9liG5Psj3ezNSZw+5puH2czDXRLcXQxf8JbJt0ejg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { "version": "20.11.16", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.16.tgz", - "integrity": "sha512-gKb0enTmRCzXSSUJDq6/sPcqrfCv2mkkG6Jt/clpn5eiCbKTY+SgZUxo+p8ZKMof5dCp9vHQUAB7wOUTod22wQ==", "dev": true, + "license": "MIT", "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/sinon": { "version": "17.0.3", - "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.3.tgz", - "integrity": "sha512-j3uovdn8ewky9kRBG19bOwaZbexJu/XjtkHyjvUgt4xfPFz18dcORIMqnYh66Fx3Powhcr85NT5+er3+oViapw==", "dev": true, + "license": "MIT", "dependencies": { "@types/sinonjs__fake-timers": "*" } }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.5", - "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.5.tgz", - "integrity": "sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/yargs": { "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } }, "node_modules/@types/yargs-parser": { "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/agent-base": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.4" }, @@ -1428,9 +986,8 @@ }, "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, + "license": "MIT", "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" @@ -1441,27 +998,24 @@ }, "node_modules/ansi-align": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.1.0" } }, "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, + "license": "MIT", "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, + "license": "MIT", "dependencies": { "type-fest": "^0.21.3" }, @@ -1474,9 +1028,8 @@ }, "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, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -1486,24 +1039,21 @@ }, "node_modules/ansi-regex": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/ansi-sequence-parser": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", - "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", - "dev": true + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -1516,9 +1066,8 @@ }, "node_modules/anymatch": { "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, + "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -1529,9 +1078,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, + "license": "MIT", "dependencies": { "default-require-extensions": "^3.0.0" }, @@ -1541,21 +1089,18 @@ }, "node_modules/archy": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true + "dev": true, + "license": "MIT" }, "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 + "dev": true, + "license": "Python-2.0" }, "node_modules/array-buffer-byte-length": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", - "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.5", "is-array-buffer": "^3.0.4" @@ -1569,9 +1114,8 @@ }, "node_modules/array-union": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", "dev": true, + "license": "MIT", "dependencies": { "array-uniq": "^1.0.1" }, @@ -1581,18 +1125,16 @@ }, "node_modules/array-uniq": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/array.prototype.map": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/array.prototype.map/-/array.prototype.map-1.0.6.tgz", - "integrity": "sha512-nK1psgF2cXqP3wSyCSq0Hc7zwNq3sfljQqaG27r/7a7ooNUnn5nGq6yYWyks9jMO5EoFQ0ax80hSg6oXSRNXaw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -1609,9 +1151,8 @@ }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.5", @@ -1631,9 +1172,8 @@ }, "node_modules/ast-types": { "version": "0.13.4", - "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", - "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, + "license": "MIT", "dependencies": { "tslib": "^2.0.1" }, @@ -1643,24 +1183,21 @@ }, "node_modules/async": { "version": "3.2.5", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", - "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/async-retry": { "version": "1.3.3", - "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", - "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", "dev": true, + "license": "MIT", "dependencies": { "retry": "0.13.1" } }, "node_modules/available-typed-arrays": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.6.tgz", - "integrity": "sha512-j1QzY8iPNPG4o4xmO3ptzpRxTciqD3MgEHtifP/YnJpIo58Xu+ne4BejlbkuaLfXn/nz6HFiw29bLpj2PNMdGg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -1670,14 +1207,11 @@ }, "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 + "dev": true, + "license": "MIT" }, "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": [ { @@ -1692,37 +1226,34 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/basic-ftp": { "version": "5.0.4", - "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.0.4.tgz", - "integrity": "sha512-8PzkB0arJFV4jJWSGOYR+OEic6aeKMu/osRhBULN6RY0ykby6LKhbmuQ5ublvaas5BOwboah5D87nrHyuh8PPA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/before-after-hook": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", - "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "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, + "license": "MIT", "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, + "license": "MIT", "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -1731,9 +1262,8 @@ }, "node_modules/boxen": { "version": "7.1.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", - "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", "dev": true, + "license": "MIT", "dependencies": { "ansi-align": "^3.0.1", "camelcase": "^7.0.1", @@ -1753,9 +1283,8 @@ }, "node_modules/boxen/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1765,9 +1294,8 @@ }, "node_modules/boxen/node_modules/ansi-styles": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -1777,9 +1305,8 @@ }, "node_modules/boxen/node_modules/camelcase": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -1789,9 +1316,8 @@ }, "node_modules/boxen/node_modules/chalk": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -1801,15 +1327,13 @@ }, "node_modules/boxen/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/boxen/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -1824,9 +1348,8 @@ }, "node_modules/boxen/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -1839,9 +1362,8 @@ }, "node_modules/boxen/node_modules/type-fest": { "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -1851,9 +1373,8 @@ }, "node_modules/boxen/node_modules/wrap-ansi": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", @@ -1868,9 +1389,8 @@ }, "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, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -1878,9 +1398,8 @@ }, "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, + "license": "MIT", "dependencies": { "fill-range": "^7.0.1" }, @@ -1890,14 +1409,11 @@ }, "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 + "dev": true, + "license": "ISC" }, "node_modules/browserslist": { "version": "4.22.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", - "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", "dev": true, "funding": [ { @@ -1913,6 +1429,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "caniuse-lite": "^1.0.30001580", "electron-to-chromium": "^1.4.648", @@ -1928,8 +1445,6 @@ }, "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": [ { @@ -1945,6 +1460,7 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -1952,9 +1468,8 @@ }, "node_modules/bundle-name": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", "dev": true, + "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" }, @@ -1967,18 +1482,16 @@ }, "node_modules/cacheable-lookup": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" } }, "node_modules/cacheable-request": { "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", @@ -1994,9 +1507,8 @@ }, "node_modules/cacheable-request/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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2006,9 +1518,8 @@ }, "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, + "license": "MIT", "dependencies": { "hasha": "^5.0.0", "make-dir": "^3.0.0", @@ -2021,9 +1532,8 @@ }, "node_modules/call-bind": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", - "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2", "get-intrinsic": "^1.2.1", @@ -2035,26 +1545,22 @@ }, "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, + "license": "MIT", "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, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { "version": "1.0.30001584", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001584.tgz", - "integrity": "sha512-LOz7CCQ9M1G7OjJOF9/mzmqmj3jE/7VOmrfw6Mgs0E8cjOsbRXQJHsPBfmBOXDskXKrHLyyW3n7kpDW/4BsfpQ==", "dev": true, "funding": [ { @@ -2069,13 +1575,13 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "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, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2089,9 +1595,8 @@ }, "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, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -2101,14 +1606,11 @@ }, "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 + "dev": true, + "license": "MIT" }, "node_modules/chokidar": { "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", "dev": true, "funding": [ { @@ -2116,6 +1618,7 @@ "url": "https://paulmillr.com/funding/" } ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2134,8 +1637,6 @@ }, "node_modules/ci-info": { "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, "funding": [ { @@ -2143,24 +1644,23 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } }, "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, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/cli-boxes": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2170,9 +1670,8 @@ }, "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, + "license": "MIT", "dependencies": { "restore-cursor": "^3.1.0" }, @@ -2182,9 +1681,8 @@ }, "node_modules/cli-spinners": { "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" }, @@ -2194,18 +1692,16 @@ }, "node_modules/cli-width": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, + "license": "ISC", "engines": { "node": ">= 12" } }, "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, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -2214,9 +1710,8 @@ }, "node_modules/cliui/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, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -2231,26 +1726,23 @@ }, "node_modules/clone": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" } }, "node_modules/cluster-key-slot": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", "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, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2260,36 +1752,31 @@ }, "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 + "dev": true, + "license": "MIT" }, "node_modules/commander": { "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" } }, "node_modules/commondir": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/concat-map": { "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/config-chain": { "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "dev": true, + "license": "MIT", "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -2297,15 +1784,13 @@ }, "node_modules/config-chain/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 + "dev": true, + "license": "ISC" }, "node_modules/configstore": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", - "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dot-prop": "^6.0.1", "graceful-fs": "^4.2.6", @@ -2322,15 +1807,13 @@ }, "node_modules/convert-source-map": { "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/cosmiconfig": { "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, + "license": "MIT", "dependencies": { "env-paths": "^2.2.1", "import-fresh": "^3.3.0", @@ -2354,9 +1837,8 @@ }, "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, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2368,9 +1850,8 @@ }, "node_modules/crypto-random-string": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^1.0.1" }, @@ -2383,9 +1864,8 @@ }, "node_modules/crypto-random-string/node_modules/type-fest": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -2395,18 +1875,16 @@ }, "node_modules/data-uri-to-buffer": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 12" } }, "node_modules/debug": { "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "2.1.2" }, @@ -2421,24 +1899,21 @@ }, "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 + "dev": true, + "license": "MIT" }, "node_modules/decamelize": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "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, + "license": "MIT", "dependencies": { "mimic-response": "^3.1.0" }, @@ -2451,9 +1926,8 @@ }, "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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2463,18 +1937,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=4.0.0" } }, "node_modules/default-browser": { "version": "5.2.1", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", - "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", "dev": true, + "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" @@ -2488,9 +1960,8 @@ }, "node_modules/default-browser-id": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", - "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -2500,9 +1971,8 @@ }, "node_modules/default-require-extensions": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/default-require-extensions/-/default-require-extensions-3.0.1.tgz", - "integrity": "sha512-eXTJmRbm2TIt9MgWTsOH1wEuhew6XGZcMeGKCtLedIg/NCsg1iBePXkceTdK4Fii7pzmN9tGsZhKzZ4h7O/fxw==", "dev": true, + "license": "MIT", "dependencies": { "strip-bom": "^4.0.0" }, @@ -2515,9 +1985,8 @@ }, "node_modules/defaults": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", "dev": true, + "license": "MIT", "dependencies": { "clone": "^1.0.2" }, @@ -2527,18 +1996,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=10" } }, "node_modules/define-data-property": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", - "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", "dev": true, + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.1", "gopd": "^1.0.1", @@ -2550,9 +2017,8 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2562,9 +2028,8 @@ }, "node_modules/define-properties": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", @@ -2579,9 +2044,8 @@ }, "node_modules/degenerator": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", - "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", "dev": true, + "license": "MIT", "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", @@ -2593,24 +2057,21 @@ }, "node_modules/deprecation": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", - "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==", - "dev": true + "dev": true, + "license": "ISC" }, "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, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "node_modules/dot-prop": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", "dev": true, + "license": "MIT", "dependencies": { "is-obj": "^2.0.0" }, @@ -2623,51 +2084,44 @@ }, "node_modules/eastasianwidth": { "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/electron-to-chromium": { "version": "1.4.656", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.656.tgz", - "integrity": "sha512-9AQB5eFTHyR3Gvt2t/NwR0le2jBSUNwCnMbUCejFWHD+so4tH40/dRLgoE+jxlPeWS43XJewyvCv+I8LPMl49Q==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/email-addresses": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/email-addresses/-/email-addresses-5.0.0.tgz", - "integrity": "sha512-4OIPYlA6JXqtVn8zpHpGiI7vE6EQOAg16aGnDMIAlZVinnoZ8208tW1hAbjWydgN/4PLTT9q+O1K6AH/vALJGw==", - "dev": true + "dev": true, + "license": "MIT" }, "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 + "dev": true, + "license": "MIT" }, "node_modules/env-paths": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", - "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "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, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-abstract": { "version": "1.22.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.3.tgz", - "integrity": "sha512-eiiY8HQeYfYH2Con2berK+To6GrK2RxbPawDkGq4UiCQQfZHb6wX9qQqkbpPqaxQFcl8d9QzZqo0tGE0VcrdwA==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.0", "arraybuffer.prototype.slice": "^1.0.2", @@ -2718,24 +2172,21 @@ }, "node_modules/es-array-method-boxes-properly": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/es-errors": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/es-get-iterator": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -2753,9 +2204,8 @@ }, "node_modules/es-set-tostringtag": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz", - "integrity": "sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==", "dev": true, + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.2", "has-tostringtag": "^1.0.0", @@ -2767,9 +2217,8 @@ }, "node_modules/es-to-primitive": { "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "dev": true, + "license": "MIT", "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", @@ -2784,16 +2233,14 @@ }, "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 + "dev": true, + "license": "MIT" }, "node_modules/esbuild": { "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -2828,18 +2275,16 @@ }, "node_modules/escalade": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/escape-goat": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -2849,9 +2294,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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -2861,9 +2305,8 @@ }, "node_modules/escodegen": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", - "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", @@ -2882,9 +2325,8 @@ }, "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, + "license": "BSD-2-Clause", "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -2895,27 +2337,24 @@ }, "node_modules/estraverse": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } }, "node_modules/esutils": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.10.0" } }, "node_modules/execa": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", @@ -2936,9 +2375,8 @@ }, "node_modules/execa/node_modules/is-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -2948,9 +2386,8 @@ }, "node_modules/execa/node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -2960,9 +2397,8 @@ }, "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, + "license": "MIT", "dependencies": { "chardet": "^0.7.0", "iconv-lite": "^0.4.24", @@ -2974,9 +2410,8 @@ }, "node_modules/fast-glob": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", @@ -2990,17 +2425,14 @@ }, "node_modules/fastq": { "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, "node_modules/fetch-blob": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", "dev": true, "funding": [ { @@ -3012,6 +2444,7 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" @@ -3022,9 +2455,8 @@ }, "node_modules/figures": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-5.0.0.tgz", - "integrity": "sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^5.0.0", "is-unicode-supported": "^1.2.0" @@ -3038,9 +2470,8 @@ }, "node_modules/figures/node_modules/escape-string-regexp": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3050,9 +2481,8 @@ }, "node_modules/figures/node_modules/is-unicode-supported": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -3062,18 +2492,16 @@ }, "node_modules/filename-reserved-regex": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha512-lc1bnsSr4L4Bdif8Xb/qrtokGbq5zlsms/CYH8PP+WtCkGNF65DPiQY8vG3SakEdRn8Dlnm+gW/qWKKjS5sZzQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/filenamify": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", - "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", "dev": true, + "license": "MIT", "dependencies": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.1", @@ -3088,9 +2516,8 @@ }, "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, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -3100,9 +2527,8 @@ }, "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, + "license": "MIT", "dependencies": { "commondir": "^1.0.1", "make-dir": "^3.0.2", @@ -3117,9 +2543,8 @@ }, "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, + "license": "MIT", "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -3133,27 +2558,24 @@ }, "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, + "license": "BSD-3-Clause", "bin": { "flat": "cli.js" } }, "node_modules/for-each": { "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", "dev": true, + "license": "MIT", "dependencies": { "is-callable": "^1.1.3" } }, "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, + "license": "ISC", "dependencies": { "cross-spawn": "^7.0.0", "signal-exit": "^3.0.2" @@ -3164,18 +2586,16 @@ }, "node_modules/form-data-encoder": { "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14.17" } }, "node_modules/formdata-polyfill": { "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", "dev": true, + "license": "MIT", "dependencies": { "fetch-blob": "^3.1.2" }, @@ -3185,8 +2605,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": [ { @@ -3201,13 +2619,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/fs-extra": { "version": "11.2.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", - "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -3219,38 +2637,21 @@ }, "node_modules/fs.realpath": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } + "license": "ISC" }, "node_modules/function-bind": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/function.prototype.name": { "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -3266,36 +2667,32 @@ }, "node_modules/functions-have-names": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "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, + "license": "MIT", "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, + "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" } }, "node_modules/get-east-asian-width": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", - "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -3305,9 +2702,8 @@ }, "node_modules/get-intrinsic": { "version": "1.2.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.3.tgz", - "integrity": "sha512-JIcZczvcMVE7AUOP+X72bh8HqHBRxFdz5PDHYtNG/lE3yk9b3KZBJlwFcTyPYjg3L4RLLmZJzvjxhaZVapxFrQ==", "dev": true, + "license": "MIT", "dependencies": { "es-errors": "^1.0.0", "function-bind": "^1.1.2", @@ -3324,18 +2720,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=8.0.0" } }, "node_modules/get-stream": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", "dev": true, + "license": "MIT", "engines": { "node": ">=16" }, @@ -3345,9 +2739,8 @@ }, "node_modules/get-symbol-description": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" @@ -3361,9 +2754,8 @@ }, "node_modules/get-tsconfig": { "version": "4.7.2", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.2.tgz", - "integrity": "sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==", "dev": true, + "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -3373,9 +2765,8 @@ }, "node_modules/get-uri": { "version": "6.0.2", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.2.tgz", - "integrity": "sha512-5KLucCJobh8vBY1K07EFV4+cPZH3mrV9YeAruUseCQKHB58SGjjT2l9/eA9LD082IiuMjSlFJEcdJ27TXvbZNw==", "dev": true, + "license": "MIT", "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.0", @@ -3388,18 +2779,16 @@ }, "node_modules/get-uri/node_modules/data-uri-to-buffer": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.1.tgz", - "integrity": "sha512-MZd3VlchQkp8rdend6vrx7MmVDJzSNTBvghvKjirLkD+WTChA3KUf0jkE68Q4UyctNqI11zZO9/x2Yx+ub5Cvg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14" } }, "node_modules/get-uri/node_modules/fs-extra": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", @@ -3411,27 +2800,24 @@ }, "node_modules/get-uri/node_modules/jsonfile": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", "dev": true, + "license": "MIT", "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "node_modules/get-uri/node_modules/universalify": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4.0.0" } }, "node_modules/gh-pages": { "version": "6.1.1", - "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.1.1.tgz", - "integrity": "sha512-upnohfjBwN5hBP9w2dPE7HO5JJTHzSGMV1JrLrHvNuqmjoYHg6TBrCcnEoorjG/e0ejbuvnwyKMdTyM40PEByw==", "dev": true, + "license": "MIT", "dependencies": { "async": "^3.2.4", "commander": "^11.0.0", @@ -3451,9 +2837,8 @@ }, "node_modules/git-up": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", - "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", "dev": true, + "license": "MIT", "dependencies": { "is-ssh": "^1.4.0", "parse-url": "^8.1.0" @@ -3461,18 +2846,16 @@ }, "node_modules/git-url-parse": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-14.0.0.tgz", - "integrity": "sha512-NnLweV+2A4nCvn4U/m2AoYu0pPKlsmhK9cknG7IMwsjFY1S2jxM+mAhsDxyxfCIGfGaD+dozsyX4b6vkYc83yQ==", "dev": true, + "license": "MIT", "dependencies": { "git-up": "^7.0.0" } }, "node_modules/glob": { "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -3490,9 +2873,8 @@ }, "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, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3502,9 +2884,8 @@ }, "node_modules/global-dirs": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", "dev": true, + "license": "MIT", "dependencies": { "ini": "2.0.0" }, @@ -3517,18 +2898,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/globalthis": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz", - "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==", "dev": true, + "license": "MIT", "dependencies": { "define-properties": "^1.1.3" }, @@ -3541,9 +2920,8 @@ }, "node_modules/globby": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", "dev": true, + "license": "MIT", "dependencies": { "array-union": "^1.0.1", "glob": "^7.0.3", @@ -3557,9 +2935,8 @@ }, "node_modules/gopd": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", "dev": true, + "license": "MIT", "dependencies": { "get-intrinsic": "^1.1.3" }, @@ -3569,9 +2946,8 @@ }, "node_modules/got": { "version": "13.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", - "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", @@ -3594,9 +2970,8 @@ }, "node_modules/got/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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -3606,33 +2981,29 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/has-bigints": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "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, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/has-property-descriptors": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", - "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", "dev": true, + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.2" }, @@ -3642,9 +3013,8 @@ }, "node_modules/has-proto": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", - "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3654,9 +3024,8 @@ }, "node_modules/has-symbols": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3666,9 +3035,8 @@ }, "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, + "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" }, @@ -3681,9 +3049,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, + "license": "MIT", "dependencies": { "is-stream": "^2.0.0", "type-fest": "^0.8.0" @@ -3697,9 +3064,8 @@ }, "node_modules/hasown": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", - "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", "dev": true, + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3709,30 +3075,26 @@ }, "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, + "license": "MIT", "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 + "dev": true, + "license": "MIT" }, "node_modules/http-cache-semantics": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", - "dev": true + "dev": true, + "license": "BSD-2-Clause" }, "node_modules/http-proxy-agent": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.0.tgz", - "integrity": "sha512-+ZT+iBxVUQ1asugqnD6oWoRiS25AkjNfG085dKJGtGxkdwLQrMKU5wJr2bOOFAXzKcTuqq+7fZlTMgG3SRfIYQ==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" @@ -3743,9 +3105,8 @@ }, "node_modules/http2-wrapper": { "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", "dev": true, + "license": "MIT", "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" @@ -3756,9 +3117,8 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.2.tgz", - "integrity": "sha512-NmLNjm6ucYwtcUmL7JQC1ZQ57LmHP4lT15FQ8D61nak1rO6DH+fz5qNK2Ap5UN4ZapYICE3/0KodcLYSPsPbaA==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.0.2", "debug": "4" @@ -3769,18 +3129,16 @@ }, "node_modules/human-signals": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=16.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, + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" }, @@ -3790,8 +3148,6 @@ }, "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": [ { @@ -3806,22 +3162,21 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "BSD-3-Clause" }, "node_modules/ignore": { "version": "5.3.1", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "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, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3835,45 +3190,40 @@ }, "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, + "license": "MIT", "engines": { "node": ">=4" } }, "node_modules/import-lazy": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/imurmurhash": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "license": "MIT", "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, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/inflight": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3881,24 +3231,21 @@ }, "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 + "dev": true, + "license": "ISC" }, "node_modules/ini": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/inquirer": { "version": "9.2.12", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.2.12.tgz", - "integrity": "sha512-mg3Fh9g2zfuVWJn6lhST0O7x4n03k7G8Tx5nvikJkbq8/CK47WDVm+UznF0G6s5Zi0KcyUisr6DU8T67N5U+1Q==", "dev": true, + "license": "MIT", "dependencies": { "@ljharb/through": "^2.3.11", "ansi-escapes": "^4.3.2", @@ -3922,9 +3269,8 @@ }, "node_modules/inquirer/node_modules/chalk": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -3934,18 +3280,16 @@ }, "node_modules/inquirer/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, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/inquirer/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, + "license": "MIT", "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", @@ -3966,9 +3310,8 @@ }, "node_modules/inquirer/node_modules/ora/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, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -3982,9 +3325,8 @@ }, "node_modules/inquirer/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, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -3994,9 +3336,8 @@ }, "node_modules/internal-slot": { "version": "1.0.6", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.6.tgz", - "integrity": "sha512-Xj6dv+PsbtwyPpEflsejS+oIZxmMlV44zAhG479uYu89MsjcYOhCFnNyKrkJrihbsiasQyY0afoCl/9BLR65bg==", "dev": true, + "license": "MIT", "dependencies": { "get-intrinsic": "^1.2.2", "hasown": "^2.0.0", @@ -4008,24 +3349,21 @@ }, "node_modules/interpret": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.10" } }, "node_modules/ip": { "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-arguments": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4039,9 +3377,8 @@ }, "node_modules/is-array-buffer": { "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.1" @@ -4055,15 +3392,13 @@ }, "node_modules/is-arrayish": { "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-bigint": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, + "license": "MIT", "dependencies": { "has-bigints": "^1.0.1" }, @@ -4073,9 +3408,8 @@ }, "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, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -4085,9 +3419,8 @@ }, "node_modules/is-boolean-object": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4101,9 +3434,8 @@ }, "node_modules/is-callable": { "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4113,9 +3445,8 @@ }, "node_modules/is-ci": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", "dev": true, + "license": "MIT", "dependencies": { "ci-info": "^3.2.0" }, @@ -4125,9 +3456,8 @@ }, "node_modules/is-core-module": { "version": "2.13.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", - "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", "dev": true, + "license": "MIT", "dependencies": { "hasown": "^2.0.0" }, @@ -4137,9 +3467,8 @@ }, "node_modules/is-date-object": { "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -4152,9 +3481,8 @@ }, "node_modules/is-docker": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", "dev": true, + "license": "MIT", "bin": { "is-docker": "cli.js" }, @@ -4167,27 +3495,24 @@ }, "node_modules/is-extglob": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "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, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-glob": { "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -4197,9 +3522,8 @@ }, "node_modules/is-in-ci": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-in-ci/-/is-in-ci-0.1.0.tgz", - "integrity": "sha512-d9PXLEY0v1iJ64xLiQMJ51J128EYHAaOR4yZqQi8aHGfw6KgifM3/Viw1oZZ1GCVmb3gBuyhLyHj0HgR2DhSXQ==", "dev": true, + "license": "MIT", "bin": { "is-in-ci": "cli.js" }, @@ -4212,9 +3536,8 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", "dev": true, + "license": "MIT", "dependencies": { "is-docker": "^3.0.0" }, @@ -4230,9 +3553,8 @@ }, "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, + "license": "MIT", "dependencies": { "global-dirs": "^3.0.0", "is-path-inside": "^3.0.2" @@ -4246,9 +3568,8 @@ }, "node_modules/is-interactive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", - "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -4258,18 +3579,16 @@ }, "node_modules/is-map": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-negative-zero": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -4279,9 +3598,8 @@ }, "node_modules/is-npm": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", - "integrity": "sha512-JEjxbSmtPSt1c8XTkVrlujcXdKV1/tvuQ7GwKcAlyiVLeYFQ2VHat8xfrDJsIkhCdF/tZ7CiIR3sy141c6+gPQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -4291,18 +3609,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, "node_modules/is-number-object": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", "dev": true, + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -4315,36 +3631,32 @@ }, "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, + "license": "MIT", "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, + "license": "MIT", "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, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/is-regex": { "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" @@ -4358,18 +3670,16 @@ }, "node_modules/is-set": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-shared-array-buffer": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2" }, @@ -4379,18 +3689,16 @@ }, "node_modules/is-ssh": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", - "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", "dev": true, + "license": "MIT", "dependencies": { "protocols": "^2.0.1" } }, "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, + "license": "MIT", "engines": { "node": ">=8" }, @@ -4400,9 +3708,8 @@ }, "node_modules/is-string": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dev": true, + "license": "MIT", "dependencies": { "has-tostringtag": "^1.0.0" }, @@ -4415,9 +3722,8 @@ }, "node_modules/is-symbol": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "dev": true, + "license": "MIT", "dependencies": { "has-symbols": "^1.0.2" }, @@ -4430,9 +3736,8 @@ }, "node_modules/is-typed-array": { "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", "dev": true, + "license": "MIT", "dependencies": { "which-typed-array": "^1.1.14" }, @@ -4445,15 +3750,13 @@ }, "node_modules/is-typedarray": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -4463,9 +3766,8 @@ }, "node_modules/is-weakref": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2" }, @@ -4475,18 +3777,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/is-wsl": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, + "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" }, @@ -4499,21 +3799,18 @@ }, "node_modules/isarray": { "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/isexe": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/issue-parser": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-6.0.0.tgz", - "integrity": "sha512-zKa/Dxq2lGsBIXQ7CUZWTHfvxPC2ej0KfO7fIPqLlHB9J2hJ7rGhZ5rilhuufylr4RXYPzJUeFjKxz305OsNlA==", "dev": true, + "license": "MIT", "dependencies": { "lodash.capitalize": "^4.2.1", "lodash.escaperegexp": "^4.1.2", @@ -4527,18 +3824,16 @@ }, "node_modules/istanbul-lib-coverage": { "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "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, + "license": "BSD-3-Clause", "dependencies": { "append-transform": "^2.0.0" }, @@ -4548,9 +3843,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, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.7.5", "@istanbuljs/schema": "^0.1.2", @@ -4563,9 +3857,8 @@ }, "node_modules/istanbul-lib-processinfo": { "version": "2.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-processinfo/-/istanbul-lib-processinfo-2.0.3.tgz", - "integrity": "sha512-NkwHbo3E00oybX6NGJi6ar0B29vxyvNwoC7eJ4G4Yq28UfY758Hgn/heV8VRFhevPED4LXfFz0DQ8z/0kw9zMg==", "dev": true, + "license": "ISC", "dependencies": { "archy": "^1.0.0", "cross-spawn": "^7.0.3", @@ -4580,9 +3873,8 @@ }, "node_modules/istanbul-lib-report": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -4594,9 +3886,8 @@ }, "node_modules/istanbul-lib-report/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, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -4606,9 +3897,8 @@ }, "node_modules/istanbul-lib-report/node_modules/make-dir": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -4621,9 +3911,8 @@ }, "node_modules/istanbul-lib-report/node_modules/semver": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -4636,9 +3925,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, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4648,15 +3936,13 @@ }, "node_modules/istanbul-lib-report/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/istanbul-lib-source-maps": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", - "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0", @@ -4668,9 +3954,8 @@ }, "node_modules/istanbul-reports": { "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -4681,18 +3966,16 @@ }, "node_modules/iterate-iterator": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/iterate-iterator/-/iterate-iterator-1.0.2.tgz", - "integrity": "sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/iterate-value": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/iterate-value/-/iterate-value-1.0.2.tgz", - "integrity": "sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==", "dev": true, + "license": "MIT", "dependencies": { "es-get-iterator": "^1.0.2", "iterate-iterator": "^1.0.1" @@ -4703,15 +3986,13 @@ }, "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 + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -4721,9 +4002,8 @@ }, "node_modules/jsesc": { "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -4733,21 +4013,18 @@ }, "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 + "dev": true, + "license": "MIT" }, "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 + "dev": true, + "license": "MIT" }, "node_modules/json5": { "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -4757,15 +4034,13 @@ }, "node_modules/jsonc-parser": { "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/jsonfile": { "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -4775,24 +4050,21 @@ }, "node_modules/just-extend": { "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/keyv": { "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } }, "node_modules/latest-version": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", - "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", "dev": true, + "license": "MIT", "dependencies": { "package-json": "^8.1.0" }, @@ -4805,15 +4077,13 @@ }, "node_modules/lines-and-columns": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "dependencies": { "p-locate": "^5.0.0" }, @@ -4826,57 +4096,48 @@ }, "node_modules/lodash": { "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.capitalize": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", - "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", - "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.flattendeep": { "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha512-uHaJFihxmJcEX3kT4I23ABqKKalJ/zDrDg0lsFtc1h+3uw49SIJ5beyhx5ExVRti3AvKoOJngIj7xz3oylPdWQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.get": { "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", - "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lodash.uniqby": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", - "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", - "dev": true + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -4890,9 +4151,8 @@ }, "node_modules/lowercase-keys": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -4902,24 +4162,21 @@ }, "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, + "license": "ISC", "dependencies": { "yallist": "^3.0.2" } }, "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 + "dev": true, + "license": "MIT" }, "node_modules/macos-release": { "version": "3.2.0", - "resolved": "https://registry.npmjs.org/macos-release/-/macos-release-3.2.0.tgz", - "integrity": "sha512-fSErXALFNsnowREYZ49XCdOHF8wOPWuFOGQrAhP7x5J/BqQv+B02cNsTykGpDgRVx43EKg++6ANmTaGTtW+hUA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -4929,9 +4186,8 @@ }, "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, + "license": "MIT", "dependencies": { "semver": "^6.0.0" }, @@ -4944,9 +4200,8 @@ }, "node_modules/marked": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", - "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", "dev": true, + "license": "MIT", "bin": { "marked": "bin/marked.js" }, @@ -4956,24 +4211,21 @@ }, "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 + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "engines": { "node": ">= 8" } }, "node_modules/micromatch": { "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.2", "picomatch": "^2.3.1" @@ -4984,18 +4236,16 @@ }, "node_modules/mime-db": { "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "node_modules/mime-types": { "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", "dev": true, + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -5005,9 +4255,8 @@ }, "node_modules/mimic-fn": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -5017,9 +4266,8 @@ }, "node_modules/mimic-response": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -5029,9 +4277,8 @@ }, "node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5041,18 +4288,16 @@ }, "node_modules/minimist": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/mocha": { "version": "10.2.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.2.0.tgz", - "integrity": "sha512-IDY7fl/BecMwFHzoqF2sg/SHHANeBoMMXFlS9r0OXKDssYE1M5O43wUY/9BVPeIvfH2zmEbBfseqN9gBQZzXkg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-colors": "4.1.1", "browser-stdout": "1.3.1", @@ -5090,9 +4335,8 @@ }, "node_modules/mocha/node_modules/glob": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", "dev": true, + "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5110,9 +4354,8 @@ }, "node_modules/mocha/node_modules/glob/node_modules/minimatch": { "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -5122,9 +4365,8 @@ }, "node_modules/mocha/node_modules/minimatch": { "version": "5.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", - "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -5134,33 +4376,29 @@ }, "node_modules/mocha/node_modules/minimatch/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "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 + "dev": true, + "license": "MIT" }, "node_modules/mute-stream": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", "dev": true, + "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/nanoid": { "version": "3.3.3", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.3.tgz", - "integrity": "sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==", "dev": true, + "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -5170,18 +4408,16 @@ }, "node_modules/netmask": { "version": "2.0.2", - "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", - "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4.0" } }, "node_modules/new-github-release-url": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/new-github-release-url/-/new-github-release-url-2.0.0.tgz", - "integrity": "sha512-NHDDGYudnvRutt/VhKFlX26IotXe1w0cmkDm6JGquh5bz/bDTw0LufSmH/GxTjEdpHEO+bVKFTwdrcGa/9XlKQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^2.5.1" }, @@ -5194,9 +4430,8 @@ }, "node_modules/new-github-release-url/node_modules/type-fest": { "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=12.20" }, @@ -5206,9 +4441,8 @@ }, "node_modules/nise": { "version": "5.1.9", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", - "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -5219,8 +4453,6 @@ }, "node_modules/node-domexception": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "dev": true, "funding": [ { @@ -5232,15 +4464,15 @@ "url": "https://paypal.me/jimmywarting" } ], + "license": "MIT", "engines": { "node": ">=10.5.0" } }, "node_modules/node-fetch": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", "dev": true, + "license": "MIT", "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", @@ -5256,9 +4488,8 @@ }, "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, + "license": "MIT", "dependencies": { "process-on-spawn": "^1.0.0" }, @@ -5268,24 +4499,21 @@ }, "node_modules/node-releases": { "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/normalize-url": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -5295,9 +4523,8 @@ }, "node_modules/npm-run-path": { "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^4.0.0" }, @@ -5310,9 +4537,8 @@ }, "node_modules/npm-run-path/node_modules/path-key": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -5322,9 +4548,8 @@ }, "node_modules/nyc": { "version": "15.1.0", - "resolved": "https://registry.npmjs.org/nyc/-/nyc-15.1.0.tgz", - "integrity": "sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==", "dev": true, + "license": "ISC", "dependencies": { "@istanbuljs/load-nyc-config": "^1.0.0", "@istanbuljs/schema": "^0.1.2", @@ -5363,9 +4588,8 @@ }, "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, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", @@ -5374,9 +4598,8 @@ }, "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, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -5387,9 +4610,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, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -5399,9 +4621,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, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -5414,9 +4635,8 @@ }, "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, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -5426,15 +4646,13 @@ }, "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 + "dev": true, + "license": "ISC" }, "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, + "license": "MIT", "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", @@ -5454,9 +4672,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, + "license": "ISC", "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" @@ -5467,36 +4684,32 @@ }, "node_modules/object-assign": { "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/object-inspect": { "version": "1.13.1", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", - "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/object-keys": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" } }, "node_modules/object.assign": { "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.5", "define-properties": "^1.2.1", @@ -5512,18 +4725,16 @@ }, "node_modules/once": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, "node_modules/onetime": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-fn": "^4.0.0" }, @@ -5536,9 +4747,8 @@ }, "node_modules/open": { "version": "10.0.3", - "resolved": "https://registry.npmjs.org/open/-/open-10.0.3.tgz", - "integrity": "sha512-dtbI5oW7987hwC9qjJTyABldTaa19SuyJse1QboWv3b0qCcrrLNVDqBx1XgELAjh9QTVQaP/C5b1nhQebd1H2A==", "dev": true, + "license": "MIT", "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", @@ -5554,9 +4764,8 @@ }, "node_modules/ora": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-8.0.1.tgz", - "integrity": "sha512-ANIvzobt1rls2BDny5fWZ3ZVKyD6nscLvfFRpQgfWsythlcsVUC9kL0zq6j2Z5z9wwp1kd7wpsD/T9qNPVLCaQ==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^4.0.0", @@ -5577,9 +4786,8 @@ }, "node_modules/ora/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -5589,9 +4797,8 @@ }, "node_modules/ora/node_modules/chalk": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -5601,9 +4808,8 @@ }, "node_modules/ora/node_modules/cli-cursor": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-4.0.0.tgz", - "integrity": "sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^4.0.0" }, @@ -5616,15 +4822,13 @@ }, "node_modules/ora/node_modules/emoji-regex": { "version": "10.3.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz", - "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/ora/node_modules/is-unicode-supported": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.0.0.tgz", - "integrity": "sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -5634,9 +4838,8 @@ }, "node_modules/ora/node_modules/log-symbols": { "version": "6.0.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", - "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", "dev": true, + "license": "MIT", "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" @@ -5650,9 +4853,8 @@ }, "node_modules/ora/node_modules/log-symbols/node_modules/is-unicode-supported": { "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", - "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -5662,18 +4864,16 @@ }, "node_modules/ora/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, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/ora/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, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -5686,9 +4886,8 @@ }, "node_modules/ora/node_modules/restore-cursor": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-4.0.0.tgz", - "integrity": "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -5702,9 +4901,8 @@ }, "node_modules/ora/node_modules/string-width": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", - "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -5719,9 +4917,8 @@ }, "node_modules/ora/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -5734,9 +4931,8 @@ }, "node_modules/os-name": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/os-name/-/os-name-5.1.0.tgz", - "integrity": "sha512-YEIoAnM6zFmzw3PQ201gCVCIWbXNyKObGlVvpAVvraAeOHnlYVKFssbA/riRX5R40WA6kKrZ7Dr7dWzO3nKSeQ==", "dev": true, + "license": "MIT", "dependencies": { "macos-release": "^3.1.0", "windows-release": "^5.0.1" @@ -5750,27 +4946,24 @@ }, "node_modules/os-tmpdir": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/p-cancelable": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.20" } }, "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, + "license": "MIT", "dependencies": { "yocto-queue": "^0.1.0" }, @@ -5783,9 +4976,8 @@ }, "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, + "license": "MIT", "dependencies": { "p-limit": "^3.0.2" }, @@ -5798,9 +4990,8 @@ }, "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, + "license": "MIT", "dependencies": { "aggregate-error": "^3.0.0" }, @@ -5810,18 +5001,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/pac-proxy-agent": { "version": "7.0.1", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.0.1.tgz", - "integrity": "sha512-ASV8yU4LLKBAjqIPMbrgtaKIvxQri/yh2OpI+S6hVa9JRkUI3Y3NPFbfngDtY7oFtSMD3w31Xns89mDa3Feo5A==", "dev": true, + "license": "MIT", "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.0.2", @@ -5838,9 +5027,8 @@ }, "node_modules/pac-resolver": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.0.tgz", - "integrity": "sha512-Fd9lT9vJbHYRACT8OhCbZBbxr6KRSawSovFpy8nDGshaK99S/EBhVIHp9+crhxrsZOuvLpgL1n23iyPg6Rl2hg==", "dev": true, + "license": "MIT", "dependencies": { "degenerator": "^5.0.0", "ip": "^1.1.8", @@ -5852,9 +5040,8 @@ }, "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, + "license": "ISC", "dependencies": { "graceful-fs": "^4.1.15", "hasha": "^5.0.0", @@ -5867,9 +5054,8 @@ }, "node_modules/package-json": { "version": "8.1.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", - "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", "dev": true, + "license": "MIT", "dependencies": { "got": "^12.1.0", "registry-auth-token": "^5.0.1", @@ -5885,9 +5071,8 @@ }, "node_modules/package-json/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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -5897,9 +5082,8 @@ }, "node_modules/package-json/node_modules/got": { "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", @@ -5922,9 +5106,8 @@ }, "node_modules/package-json/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, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -5934,9 +5117,8 @@ }, "node_modules/package-json/node_modules/semver": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -5949,15 +5131,13 @@ }, "node_modules/package-json/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "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, + "license": "MIT", "dependencies": { "callsites": "^3.0.0" }, @@ -5967,9 +5147,8 @@ }, "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, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -5985,66 +5164,58 @@ }, "node_modules/parse-path": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", - "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", "dev": true, + "license": "MIT", "dependencies": { "protocols": "^2.0.0" } }, "node_modules/parse-url": { "version": "8.1.0", - "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", - "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", "dev": true, + "license": "MIT", "dependencies": { "parse-path": "^7.0.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, + "license": "MIT", "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": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "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, + "license": "MIT", "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 + "dev": true, + "license": "MIT" }, "node_modules/path-to-regexp": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.1.tgz", - "integrity": "sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/path-type": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -6054,15 +5225,13 @@ }, "node_modules/picocolors": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -6072,27 +5241,24 @@ }, "node_modules/pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/pinkie": { "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/pinkie-promise": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", "dev": true, + "license": "MIT", "dependencies": { "pinkie": "^2.0.0" }, @@ -6102,9 +5268,8 @@ }, "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, + "license": "MIT", "dependencies": { "find-up": "^4.0.0" }, @@ -6114,9 +5279,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, + "license": "MIT", "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" @@ -6127,9 +5291,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, + "license": "MIT", "dependencies": { "p-locate": "^4.1.0" }, @@ -6139,9 +5302,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, + "license": "MIT", "dependencies": { "p-try": "^2.0.0" }, @@ -6154,9 +5316,8 @@ }, "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, + "license": "MIT", "dependencies": { "p-limit": "^2.2.0" }, @@ -6166,9 +5327,8 @@ }, "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, + "license": "MIT", "dependencies": { "fromentries": "^1.2.0" }, @@ -6178,9 +5338,8 @@ }, "node_modules/promise.allsettled": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/promise.allsettled/-/promise.allsettled-1.0.7.tgz", - "integrity": "sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==", "dev": true, + "license": "MIT", "dependencies": { "array.prototype.map": "^1.0.5", "call-bind": "^1.0.2", @@ -6198,21 +5357,18 @@ }, "node_modules/proto-list": { "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/protocols": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", - "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/proxy-agent": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz", - "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", @@ -6229,24 +5385,21 @@ }, "node_modules/proxy-agent/node_modules/lru-cache": { "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/proxy-from-env": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/pupa": { "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.1.0.tgz", - "integrity": "sha512-FLpr4flz5xZTSJxSeaheeMKN/EDzMdK7b8PTOC6a5PYFKTucWbdqjgqaEyH0shFiSJrVB1+Qqi4Tk19ccU6Aug==", "dev": true, + "license": "MIT", "dependencies": { "escape-goat": "^4.0.0" }, @@ -6259,8 +5412,6 @@ }, "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": [ { @@ -6275,13 +5426,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -6291,18 +5442,16 @@ }, "node_modules/randombytes": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, + "license": "MIT", "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, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -6315,24 +5464,21 @@ }, "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 + "dev": true, + "license": "ISC" }, "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": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/readable-stream": { "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -6344,9 +5490,8 @@ }, "node_modules/readdirp": { "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -6356,8 +5501,6 @@ }, "node_modules/rechoir": { "version": "0.6.2", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", - "integrity": "sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==", "dev": true, "dependencies": { "resolve": "^1.1.6" @@ -6372,9 +5515,8 @@ }, "node_modules/regexp.prototype.flags": { "version": "1.5.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.1.tgz", - "integrity": "sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -6389,9 +5531,8 @@ }, "node_modules/registry-auth-token": { "version": "5.0.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.0.2.tgz", - "integrity": "sha512-o/3ikDxtXaA59BmZuZrJZDJv8NMDGSj+6j6XaeBmHw8eY1i1qd9+6H+LjVvQXx3HN6aRCGa1cUdJ9RaJZUugnQ==", "dev": true, + "license": "MIT", "dependencies": { "@pnpm/npm-conf": "^2.1.0" }, @@ -6401,9 +5542,8 @@ }, "node_modules/registry-url": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", - "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", "dev": true, + "license": "MIT", "dependencies": { "rc": "1.2.8" }, @@ -6416,8 +5556,6 @@ }, "node_modules/release-it": { "version": "17.0.3", - "resolved": "https://registry.npmjs.org/release-it/-/release-it-17.0.3.tgz", - "integrity": "sha512-QjTCmvQm91pwLEbvavEs9jofHNe8thsb9Uimin+8DNSwFRdUd73p0Owy2PP/Dzh/EegRkKq/o+4Pn1xp8pC1og==", "dev": true, "funding": [ { @@ -6429,6 +5567,7 @@ "url": "https://opencollective.com/webpro" } ], + "license": "MIT", "dependencies": { "@iarna/toml": "2.2.5", "@octokit/rest": "20.0.2", @@ -6467,9 +5606,8 @@ }, "node_modules/release-it/node_modules/chalk": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -6479,9 +5617,8 @@ }, "node_modules/release-it/node_modules/globby": { "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.0.tgz", - "integrity": "sha512-/1WM/LNHRAOH9lZta77uGbq0dAEQM+XjNesWwhlERDVenqothRbnzTrL3/LrIoEPPjeUHC3vrS6TwoyxeHs7MQ==", "dev": true, + "license": "MIT", "dependencies": { "@sindresorhus/merge-streams": "^1.0.0", "fast-glob": "^3.3.2", @@ -6499,9 +5636,8 @@ }, "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, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -6511,9 +5647,8 @@ }, "node_modules/release-it/node_modules/semver": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6526,24 +5661,21 @@ }, "node_modules/release-it/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/release-it/node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/release-zalgo": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/release-zalgo/-/release-zalgo-1.0.0.tgz", - "integrity": "sha512-gUAyHVHPPC5wdqX/LG4LWtRYtgjxyX78oanFNTMMyFEfOqdC54s3eE82imuWKbOeqYht2CrNf64Qb8vgmmtZGA==", "dev": true, + "license": "ISC", "dependencies": { "es6-error": "^4.0.1" }, @@ -6553,24 +5685,21 @@ }, "node_modules/require-directory": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, + "license": "MIT", "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 + "dev": true, + "license": "ISC" }, "node_modules/resolve": { "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, + "license": "MIT", "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", @@ -6585,33 +5714,29 @@ }, "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 + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/responselike": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", "dev": true, + "license": "MIT", "dependencies": { "lowercase-keys": "^3.0.0" }, @@ -6624,9 +5749,8 @@ }, "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, + "license": "MIT", "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -6637,18 +5761,16 @@ }, "node_modules/restore-cursor/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, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/restore-cursor/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, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -6661,18 +5783,16 @@ }, "node_modules/retry": { "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, + "license": "MIT", "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, + "license": "MIT", "engines": { "iojs": ">=1.0.0", "node": ">=0.10.0" @@ -6680,9 +5800,8 @@ }, "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, + "license": "ISC", "dependencies": { "glob": "^7.1.3" }, @@ -6695,9 +5814,8 @@ }, "node_modules/run-applescript": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", - "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -6707,17 +5825,14 @@ }, "node_modules/run-async": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", "dev": true, + "license": "MIT", "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": [ { @@ -6733,24 +5848,23 @@ "url": "https://feross.org/support" } ], + "license": "MIT", "dependencies": { "queue-microtask": "^1.2.2" } }, "node_modules/rxjs": { "version": "7.8.1", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", - "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dev": true, + "license": "Apache-2.0", "dependencies": { "tslib": "^2.1.0" } }, "node_modules/safe-array-concat": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.0.tgz", - "integrity": "sha512-ZdQ0Jeb9Ofti4hbt5lX3T2JcAamT9hfzYU1MNB+z/jaEbB6wfFfPIR/zEORmZqobkCCJhSjodobH6WHNmJ97dg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.5", "get-intrinsic": "^1.2.2", @@ -6766,8 +5880,6 @@ }, "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": [ { @@ -6782,13 +5894,13 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-regex-test": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.2.tgz", - "integrity": "sha512-83S9w6eFq12BBIJYvjMux6/dkirb8+4zJRA9cxNBVb7Wq5fJBW+Xze48WqR8pxua7bDuAaaAxtVVd4Idjp1dBQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.5", "get-intrinsic": "^1.2.2", @@ -6803,24 +5915,21 @@ }, "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 + "dev": true, + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "node_modules/semver-diff": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", - "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.3.5" }, @@ -6833,9 +5942,8 @@ }, "node_modules/semver-diff/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, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -6845,9 +5953,8 @@ }, "node_modules/semver-diff/node_modules/semver": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -6860,30 +5967,26 @@ }, "node_modules/semver-diff/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "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, + "license": "BSD-3-Clause", "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": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/set-function-length": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.0.tgz", - "integrity": "sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.1.1", "function-bind": "^1.1.2", @@ -6897,9 +6000,8 @@ }, "node_modules/set-function-name": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.1.tgz", - "integrity": "sha512-tMNCiqYVkXIZgc2Hnoy2IvC/f8ezc5koaRFkCjrpWzGpCd3qbZXPzVy9MAZzK1ch/X0jvSkojys3oqJN0qCmdA==", "dev": true, + "license": "MIT", "dependencies": { "define-data-property": "^1.0.1", "functions-have-names": "^1.2.3", @@ -6911,9 +6013,8 @@ }, "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, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -6923,18 +6024,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/shelljs": { "version": "0.8.5", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.8.5.tgz", - "integrity": "sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "glob": "^7.0.0", "interpret": "^1.0.0", @@ -6949,9 +6048,8 @@ }, "node_modules/shiki": { "version": "0.14.7", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.7.tgz", - "integrity": "sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-sequence-parser": "^1.1.0", "jsonc-parser": "^3.2.0", @@ -6961,9 +6059,8 @@ }, "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, + "license": "MIT", "dependencies": { "call-bind": "^1.0.0", "get-intrinsic": "^1.0.2", @@ -6975,15 +6072,13 @@ }, "node_modules/signal-exit": { "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/sinon": { "version": "17.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-17.0.1.tgz", - "integrity": "sha512-wmwE19Lie0MLT+ZYNpDymasPHUKTaZHUH/pKEubRXIzySv9Atnlw+BUMGCzWgV7b7wO+Hw6f1TEOr0IUnmU8/g==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@sinonjs/commons": "^3.0.0", "@sinonjs/fake-timers": "^11.2.2", @@ -6999,18 +6094,16 @@ }, "node_modules/sinon/node_modules/diff": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.1.0.tgz", - "integrity": "sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" } }, "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, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7020,9 +6113,8 @@ }, "node_modules/slash": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.16" }, @@ -7032,9 +6124,8 @@ }, "node_modules/smart-buffer": { "version": "4.2.0", - "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", - "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 6.0.0", "npm": ">= 3.0.0" @@ -7042,9 +6133,8 @@ }, "node_modules/socks": { "version": "2.7.1", - "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", - "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", "dev": true, + "license": "MIT", "dependencies": { "ip": "^2.0.0", "smart-buffer": "^4.2.0" @@ -7056,9 +6146,8 @@ }, "node_modules/socks-proxy-agent": { "version": "8.0.2", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.2.tgz", - "integrity": "sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.0.2", "debug": "^4.3.4", @@ -7070,24 +6159,21 @@ }, "node_modules/socks/node_modules/ip": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", - "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", - "dev": true + "dev": true, + "license": "MIT" }, "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, + "license": "BSD-3-Clause", "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, + "license": "ISC", "dependencies": { "foreground-child": "^2.0.0", "is-windows": "^1.0.2", @@ -7102,15 +6188,13 @@ }, "node_modules/sprintf-js": { "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "dev": true + "dev": true, + "license": "BSD-3-Clause" }, "node_modules/stdin-discarder": { "version": "0.2.2", - "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", - "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -7120,9 +6204,8 @@ }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", "dev": true, + "license": "MIT", "dependencies": { "internal-slot": "^1.0.4" }, @@ -7132,18 +6215,16 @@ }, "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, + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/string-width": { "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", @@ -7155,9 +6236,8 @@ }, "node_modules/string.prototype.trim": { "version": "1.2.8", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz", - "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -7172,9 +6252,8 @@ }, "node_modules/string.prototype.trimend": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz", - "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -7186,9 +6265,8 @@ }, "node_modules/string.prototype.trimstart": { "version": "1.0.7", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz", - "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", @@ -7200,9 +6278,8 @@ }, "node_modules/strip-ansi": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" }, @@ -7212,18 +6289,16 @@ }, "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, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/strip-final-newline": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -7233,9 +6308,8 @@ }, "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, + "license": "MIT", "engines": { "node": ">=8" }, @@ -7245,9 +6319,8 @@ }, "node_modules/strip-outer": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.2" }, @@ -7257,18 +6330,16 @@ }, "node_modules/strip-outer/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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "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, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -7281,9 +6352,8 @@ }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -7293,9 +6363,8 @@ }, "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, + "license": "ISC", "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", @@ -7307,9 +6376,8 @@ }, "node_modules/tmp": { "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, + "license": "MIT", "dependencies": { "os-tmpdir": "~1.0.2" }, @@ -7319,18 +6387,16 @@ }, "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": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, "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, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -7340,9 +6406,8 @@ }, "node_modules/trim-repeated": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", - "integrity": "sha512-pkonvlKk8/ZuR0D5tLW8ljt5I8kmxp2XKymhepUeOdCEfKpZaktSArkLHZt76OB1ZvO9bssUsDty4SWhLvZpLg==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^1.0.2" }, @@ -7352,24 +6417,21 @@ }, "node_modules/trim-repeated/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": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8.0" } }, "node_modules/tslib": { "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "dev": true, + "license": "0BSD" }, "node_modules/tsx": { "version": "4.7.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.7.0.tgz", - "integrity": "sha512-I+t79RYPlEYlHn9a+KzwrvEwhJg35h/1zHsLC2JXvhC2mdynMv6Zxzvhv5EMV6VF5qJlLlkSnMVvdZV3PSIGcg==", "dev": true, + "license": "MIT", "dependencies": { "esbuild": "~0.19.10", "get-tsconfig": "^4.7.2" @@ -7386,27 +6448,24 @@ }, "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, + "license": "MIT", "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, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=8" } }, "node_modules/typed-array-buffer": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.0.tgz", - "integrity": "sha512-Y8KTSIglk9OZEr8zywiIHG/kmQ7KWyjseXs1CbSo8vC42w7hg2HgYTxSWwP0+is7bWDc1H+Fo026CpHFwm8tkw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.1", @@ -7418,9 +6477,8 @@ }, "node_modules/typed-array-byte-length": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.0.tgz", - "integrity": "sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -7436,9 +6494,8 @@ }, "node_modules/typed-array-byte-offset": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.0.tgz", - "integrity": "sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", @@ -7455,9 +6512,8 @@ }, "node_modules/typed-array-length": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.4.tgz", - "integrity": "sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "for-each": "^0.3.3", @@ -7469,18 +6525,16 @@ }, "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, + "license": "MIT", "dependencies": { "is-typedarray": "^1.0.0" } }, "node_modules/typedoc": { "version": "0.25.7", - "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.25.7.tgz", - "integrity": "sha512-m6A6JjQRg39p2ZVRIN3NKXgrN8vzlHhOS+r9ymUYtcUP/TIQPvWSq7YgE5ZjASfv5Vd5BW5xrir6Gm2XNNcOow==", "dev": true, + "license": "Apache-2.0", "dependencies": { "lunr": "^2.3.9", "marked": "^4.3.0", @@ -7499,18 +6553,16 @@ }, "node_modules/typedoc/node_modules/brace-expansion": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "node_modules/typedoc/node_modules/minimatch": { "version": "9.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", - "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -7523,9 +6575,8 @@ }, "node_modules/typescript": { "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7536,9 +6587,8 @@ }, "node_modules/unbox-primitive": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", @@ -7551,15 +6601,13 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/unicorn-magic": { "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -7569,9 +6617,8 @@ }, "node_modules/unique-string": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", "dev": true, + "license": "MIT", "dependencies": { "crypto-random-string": "^4.0.0" }, @@ -7584,23 +6631,19 @@ }, "node_modules/universal-user-agent": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", - "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } }, "node_modules/update-browserslist-db": { "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", "dev": true, "funding": [ { @@ -7616,6 +6659,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.1.1", "picocolors": "^1.0.0" @@ -7629,9 +6673,8 @@ }, "node_modules/update-notifier": { "version": "7.0.0", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-7.0.0.tgz", - "integrity": "sha512-Hv25Bh+eAbOLlsjJreVPOs4vd51rrtCrmhyOJtbpAojro34jS4KQaEp4/EvlHJX7jSO42VvEFpkastVyXyIsdQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boxen": "^7.1.1", "chalk": "^5.3.0", @@ -7655,9 +6698,8 @@ }, "node_modules/update-notifier/node_modules/chalk": { "version": "5.3.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", - "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", "dev": true, + "license": "MIT", "engines": { "node": "^12.17.0 || ^14.13 || >=16.0.0" }, @@ -7667,9 +6709,8 @@ }, "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, + "license": "ISC", "dependencies": { "yallist": "^4.0.0" }, @@ -7679,9 +6720,8 @@ }, "node_modules/update-notifier/node_modules/semver": { "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^6.0.0" }, @@ -7694,69 +6734,60 @@ }, "node_modules/update-notifier/node_modules/yallist": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/url-join": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", - "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, "node_modules/util-deprecate": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true + "dev": true, + "license": "MIT" }, "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, + "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, "node_modules/vscode-oniguruma": { "version": "1.7.0", - "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", - "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/vscode-textmate": { "version": "8.0.0", - "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", - "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/wcwidth": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", "dev": true, + "license": "MIT", "dependencies": { "defaults": "^1.0.3" } }, "node_modules/web-streams-polyfill": { "version": "3.3.2", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", - "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 8" } }, "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, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -7769,9 +6800,8 @@ }, "node_modules/which-boxed-primitive": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", "dev": true, + "license": "MIT", "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", @@ -7785,15 +6815,13 @@ }, "node_modules/which-module": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", - "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/which-typed-array": { "version": "1.1.14", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.14.tgz", - "integrity": "sha512-VnXFiIW8yNn9kIHN88xvZ4yOWchftKDsRJ8fEPacX/wl1lOvBrhsJ/OeJCXq7B0AaijRuqgzSKalJoPk+D8MPg==", "dev": true, + "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.6", "call-bind": "^1.0.5", @@ -7810,9 +6838,8 @@ }, "node_modules/widest-line": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", "dev": true, + "license": "MIT", "dependencies": { "string-width": "^5.0.1" }, @@ -7825,9 +6852,8 @@ }, "node_modules/widest-line/node_modules/ansi-regex": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", - "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -7837,15 +6863,13 @@ }, "node_modules/widest-line/node_modules/emoji-regex": { "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/widest-line/node_modules/string-width": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", "dev": true, + "license": "MIT", "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", @@ -7860,9 +6884,8 @@ }, "node_modules/widest-line/node_modules/strip-ansi": { "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -7875,15 +6898,13 @@ }, "node_modules/wildcard-match": { "version": "5.1.2", - "resolved": "https://registry.npmjs.org/wildcard-match/-/wildcard-match-5.1.2.tgz", - "integrity": "sha512-qNXwI591Z88c8bWxp+yjV60Ch4F8Riawe3iGxbzquhy8Xs9m+0+SLFBGb/0yCTIDElawtaImC37fYZ+dr32KqQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/windows-release": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/windows-release/-/windows-release-5.1.1.tgz", - "integrity": "sha512-NMD00arvqcq2nwqc5Q6KtrSRHK+fVD31erE5FEMahAw5PmVCgD7MUXodq3pdZSUkqA9Cda2iWx6s1XYwiJWRmw==", "dev": true, + "license": "MIT", "dependencies": { "execa": "^5.1.1" }, @@ -7896,9 +6917,8 @@ }, "node_modules/windows-release/node_modules/execa": { "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", "dev": true, + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -7919,9 +6939,8 @@ }, "node_modules/windows-release/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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -7931,27 +6950,24 @@ }, "node_modules/windows-release/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, + "license": "Apache-2.0", "engines": { "node": ">=10.17.0" } }, "node_modules/windows-release/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, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/windows-release/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, + "license": "MIT", "dependencies": { "path-key": "^3.0.0" }, @@ -7961,9 +6977,8 @@ }, "node_modules/windows-release/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, + "license": "MIT", "dependencies": { "mimic-fn": "^2.1.0" }, @@ -7976,24 +6991,21 @@ }, "node_modules/windows-release/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, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/workerpool": { "version": "6.2.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", - "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "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, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8005,15 +7017,13 @@ }, "node_modules/wrappy": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "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, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4", "is-typedarray": "^1.0.0", @@ -8023,9 +7033,8 @@ }, "node_modules/xdg-basedir": { "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -8035,24 +7044,21 @@ }, "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, + "license": "ISC", "engines": { "node": ">=10" } }, "node_modules/yallist": { "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true + "dev": true, + "license": "ISC" }, "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, + "license": "MIT", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", @@ -8068,18 +7074,16 @@ }, "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, + "license": "ISC", "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, + "license": "MIT", "dependencies": { "camelcase": "^6.0.0", "decamelize": "^4.0.0", @@ -8092,9 +7096,8 @@ }, "node_modules/yargs-unparser/node_modules/camelcase": { "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -8104,9 +7107,8 @@ }, "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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -8116,9 +7118,8 @@ }, "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, + "license": "MIT", "engines": { "node": ">=10" }, @@ -8128,7 +7129,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "2.0.0-next.3", + "version": "5.0.0-next.5", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8137,12 +7138,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" } }, "packages/client": { "name": "@redis/client", - "version": "2.0.0-next.4", + "version": "5.0.0-next.5", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -8158,7 +7159,7 @@ }, "packages/graph": { "name": "@redis/graph", - "version": "2.0.0-next.2", + "version": "5.0.0-next.5", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8167,12 +7168,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" } }, "packages/json": { "name": "@redis/json", - "version": "2.0.0-next.2", + "version": "5.0.0-next.5", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8181,19 +7182,19 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" } }, "packages/redis": { - "version": "5.0.0-next.4", + "version": "5.0.0-next.5", "license": "MIT", "dependencies": { - "@redis/bloom": "2.0.0-next.3", - "@redis/client": "2.0.0-next.4", - "@redis/graph": "2.0.0-next.2", - "@redis/json": "2.0.0-next.2", - "@redis/search": "2.0.0-next.2", - "@redis/time-series": "2.0.0-next.2" + "@redis/bloom": "5.0.0-next.5", + "@redis/client": "5.0.0-next.5", + "@redis/graph": "5.0.0-next.5", + "@redis/json": "5.0.0-next.5", + "@redis/search": "5.0.0-next.5", + "@redis/time-series": "5.0.0-next.5" }, "engines": { "node": ">= 18" @@ -8201,7 +7202,7 @@ }, "packages/search": { "name": "@redis/search", - "version": "2.0.0-next.2", + "version": "5.0.0-next.5", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8210,7 +7211,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" } }, "packages/test-utils": { @@ -8225,9 +7226,8 @@ }, "packages/test-utils/node_modules/cliui": { "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, + "license": "ISC", "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -8239,9 +7239,8 @@ }, "packages/test-utils/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, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8256,9 +7255,8 @@ }, "packages/test-utils/node_modules/yargs": { "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", "dev": true, + "license": "MIT", "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -8274,16 +7272,15 @@ }, "packages/test-utils/node_modules/yargs-parser": { "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, "packages/time-series": { "name": "@redis/time-series", - "version": "2.0.0-next.2", + "version": "5.0.0-next.5", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8292,7 +7289,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^2.0.0-next.4" + "@redis/client": "^5.0.0-next.5" } } } diff --git a/packages/bloom/lib/commands/bloom/ADD.spec.ts b/packages/bloom/lib/commands/bloom/ADD.spec.ts index 11267e2afdb..a229936c7df 100644 --- a/packages/bloom/lib/commands/bloom/ADD.spec.ts +++ b/packages/bloom/lib/commands/bloom/ADD.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import ADD from './ADD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.ADD', () => { it('transformArguments', () => { assert.deepEqual( - ADD.transformArguments('key', 'item'), + parseArgs(ADD, 'key', 'item'), ['BF.ADD', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/bloom/ADD.ts b/packages/bloom/lib/commands/bloom/ADD.ts index a9655754897..f394755acc4 100644 --- a/packages/bloom/lib/commands/bloom/ADD.ts +++ b/packages/bloom/lib/commands/bloom/ADD.ts @@ -1,11 +1,13 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, item: RedisArgument) { - return ['BF.ADD', key, item]; + parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { + parser.push('BF.ADD'); + parser.pushKey(key); + parser.push(item); }, transformReply: transformBooleanReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/CARD.spec.ts b/packages/bloom/lib/commands/bloom/CARD.spec.ts index b150f812574..32a28cdf6f7 100644 --- a/packages/bloom/lib/commands/bloom/CARD.spec.ts +++ b/packages/bloom/lib/commands/bloom/CARD.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import CARD from './CARD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.CARD', () => { it('transformArguments', () => { assert.deepEqual( - CARD.transformArguments('bloom'), + parseArgs(CARD, 'bloom'), ['BF.CARD', 'bloom'] ); }); diff --git a/packages/bloom/lib/commands/bloom/CARD.ts b/packages/bloom/lib/commands/bloom/CARD.ts index ddaa76cc1f8..1d206b30af9 100644 --- a/packages/bloom/lib/commands/bloom/CARD.ts +++ b/packages/bloom/lib/commands/bloom/CARD.ts @@ -1,10 +1,11 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['BF.CARD', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('BF.CARD'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/EXISTS.spec.ts b/packages/bloom/lib/commands/bloom/EXISTS.spec.ts index 7db891b92bf..4d2cc70074a 100644 --- a/packages/bloom/lib/commands/bloom/EXISTS.spec.ts +++ b/packages/bloom/lib/commands/bloom/EXISTS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import EXISTS from './EXISTS'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.EXISTS', () => { it('transformArguments', () => { assert.deepEqual( - EXISTS.transformArguments('key', 'item'), + parseArgs(EXISTS, 'key', 'item'), ['BF.EXISTS', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/bloom/EXISTS.ts b/packages/bloom/lib/commands/bloom/EXISTS.ts index 9d28d671d61..3de18dd07cd 100644 --- a/packages/bloom/lib/commands/bloom/EXISTS.ts +++ b/packages/bloom/lib/commands/bloom/EXISTS.ts @@ -1,11 +1,13 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, item: RedisArgument) { - return ['BF.EXISTS', key, item]; + parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { + parser.push('BF.EXISTS'); + parser.pushKey(key); + parser.push(item); }, transformReply: transformBooleanReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/INFO.spec.ts b/packages/bloom/lib/commands/bloom/INFO.spec.ts index 4a17dab8d33..0dbe5cb1f43 100644 --- a/packages/bloom/lib/commands/bloom/INFO.spec.ts +++ b/packages/bloom/lib/commands/bloom/INFO.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INFO from './INFO'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.INFO', () => { it('transformArguments', () => { assert.deepEqual( - INFO.transformArguments('bloom'), + parseArgs(INFO, 'bloom'), ['BF.INFO', 'bloom'] ); }); diff --git a/packages/bloom/lib/commands/bloom/INFO.ts b/packages/bloom/lib/commands/bloom/INFO.ts index 208c999b970..b1e3f137c8c 100644 --- a/packages/bloom/lib/commands/bloom/INFO.ts +++ b/packages/bloom/lib/commands/bloom/INFO.ts @@ -1,4 +1,5 @@ -import { RedisArgument, Command, UnwrapReply, NullReply, NumberReply, TuplesToMapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command, UnwrapReply, NullReply, NumberReply, TuplesToMapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; import { transformInfoV2Reply } from '.'; export type BfInfoReplyMap = TuplesToMapReply<[ @@ -9,19 +10,11 @@ export type BfInfoReplyMap = TuplesToMapReply<[ [SimpleStringReply<'Expansion rate'>, NullReply | NumberReply] ]>; -export interface BfInfoReply { - capacity: NumberReply; - size: NumberReply; - numberOfFilters: NumberReply; - numberOfInsertedItems: NumberReply; - expansionRate: NullReply | NumberReply; -} - export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['BF.INFO', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('BF.INFO'); + parser.pushKey(key); }, transformReply: { 2: (reply: UnwrapReply>, _, typeMapping?: TypeMapping): BfInfoReplyMap => { diff --git a/packages/bloom/lib/commands/bloom/INSERT.spec.ts b/packages/bloom/lib/commands/bloom/INSERT.spec.ts index ccd81e070f1..a9b544a51ae 100644 --- a/packages/bloom/lib/commands/bloom/INSERT.spec.ts +++ b/packages/bloom/lib/commands/bloom/INSERT.spec.ts @@ -1,54 +1,55 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INSERT from './INSERT'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.INSERT', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - INSERT.transformArguments('key', 'item'), + parseArgs(INSERT, 'key', 'item'), ['BF.INSERT', 'key', 'ITEMS', 'item'] ); }); it('with CAPACITY', () => { assert.deepEqual( - INSERT.transformArguments('key', 'item', { CAPACITY: 100 }), + parseArgs(INSERT, 'key', 'item', { CAPACITY: 100 }), ['BF.INSERT', 'key', 'CAPACITY', '100', 'ITEMS', 'item'] ); }); it('with ERROR', () => { assert.deepEqual( - INSERT.transformArguments('key', 'item', { ERROR: 0.01 }), + parseArgs(INSERT, 'key', 'item', { ERROR: 0.01 }), ['BF.INSERT', 'key', 'ERROR', '0.01', 'ITEMS', 'item'] ); }); it('with EXPANSION', () => { assert.deepEqual( - INSERT.transformArguments('key', 'item', { EXPANSION: 1 }), + parseArgs(INSERT, 'key', 'item', { EXPANSION: 1 }), ['BF.INSERT', 'key', 'EXPANSION', '1', 'ITEMS', 'item'] ); }); it('with NOCREATE', () => { assert.deepEqual( - INSERT.transformArguments('key', 'item', { NOCREATE: true }), + parseArgs(INSERT, 'key', 'item', { NOCREATE: true }), ['BF.INSERT', 'key', 'NOCREATE', 'ITEMS', 'item'] ); }); it('with NONSCALING', () => { assert.deepEqual( - INSERT.transformArguments('key', 'item', { NONSCALING: true }), + parseArgs(INSERT, 'key', 'item', { NONSCALING: true }), ['BF.INSERT', 'key', 'NONSCALING', 'ITEMS', 'item'] ); }); it('with CAPACITY, ERROR, EXPANSION, NOCREATE and NONSCALING', () => { assert.deepEqual( - INSERT.transformArguments('key', 'item', { + parseArgs(INSERT, 'key', 'item', { CAPACITY: 100, ERROR: 0.01, EXPANSION: 1, diff --git a/packages/bloom/lib/commands/bloom/INSERT.ts b/packages/bloom/lib/commands/bloom/INSERT.ts index dfeaf5f20de..14831f2f11a 100644 --- a/packages/bloom/lib/commands/bloom/INSERT.ts +++ b/packages/bloom/lib/commands/bloom/INSERT.ts @@ -1,6 +1,7 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; export interface BfInsertOptions { CAPACITY?: number; @@ -11,37 +12,38 @@ export interface BfInsertOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument, options?: BfInsertOptions ) { - const args = ['BF.INSERT', key]; + parser.push('BF.INSERT'); + parser.pushKey(key); if (options?.CAPACITY !== undefined) { - args.push('CAPACITY', options.CAPACITY.toString()); + parser.push('CAPACITY', options.CAPACITY.toString()); } if (options?.ERROR !== undefined) { - args.push('ERROR', options.ERROR.toString()); + parser.push('ERROR', options.ERROR.toString()); } if (options?.EXPANSION !== undefined) { - args.push('EXPANSION', options.EXPANSION.toString()); + parser.push('EXPANSION', options.EXPANSION.toString()); } if (options?.NOCREATE) { - args.push('NOCREATE'); + parser.push('NOCREATE'); } if (options?.NONSCALING) { - args.push('NONSCALING'); + parser.push('NONSCALING'); } - args.push('ITEMS'); - return pushVariadicArguments(args, items); + parser.push('ITEMS'); + parser.pushVariadic(items); }, transformReply: transformBooleanArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts b/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts index f958863c0dc..40e24f96c39 100644 --- a/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts +++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import LOADCHUNK from './LOADCHUNK'; import { RESP_TYPES } from '@redis/client'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.LOADCHUNK', () => { it('transformArguments', () => { assert.deepEqual( - LOADCHUNK.transformArguments('key', 0, ''), + parseArgs(LOADCHUNK, 'key', 0, ''), ['BF.LOADCHUNK', 'key', '0', ''] ); }); diff --git a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts index feade2fac4c..47036e042af 100644 --- a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts @@ -1,10 +1,12 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, iterator: number, chunk: RedisArgument) { - return ['BF.LOADCHUNK', key, iterator.toString(), chunk]; + parseCommand(parser: CommandParser, key: RedisArgument, iterator: number, chunk: RedisArgument) { + parser.push('BF.LOADCHUNK'); + parser.pushKey(key); + parser.push(iterator.toString(), chunk); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/MADD.spec.ts b/packages/bloom/lib/commands/bloom/MADD.spec.ts index 5241a09485a..5eb39ee73d4 100644 --- a/packages/bloom/lib/commands/bloom/MADD.spec.ts +++ b/packages/bloom/lib/commands/bloom/MADD.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import MADD from './MADD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.MADD', () => { it('transformArguments', () => { assert.deepEqual( - MADD.transformArguments('key', ['1', '2']), + parseArgs(MADD, 'key', ['1', '2']), ['BF.MADD', 'key', '1', '2'] ); }); diff --git a/packages/bloom/lib/commands/bloom/MADD.ts b/packages/bloom/lib/commands/bloom/MADD.ts index afb122476fd..fda7419bf19 100644 --- a/packages/bloom/lib/commands/bloom/MADD.ts +++ b/packages/bloom/lib/commands/bloom/MADD.ts @@ -1,12 +1,14 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, items: RedisVariadicArgument) { - return pushVariadicArguments(['BF.MADD', key], items); + parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { + parser.push('BF.MADD'); + parser.pushKey(key); + parser.pushVariadic(items); }, transformReply: transformBooleanArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts b/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts index 0f313ba636f..60c09b00f17 100644 --- a/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts +++ b/packages/bloom/lib/commands/bloom/MEXISTS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import MEXISTS from './MEXISTS'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.MEXISTS', () => { it('transformArguments', () => { assert.deepEqual( - MEXISTS.transformArguments('key', ['1', '2']), + parseArgs(MEXISTS, 'key', ['1', '2']), ['BF.MEXISTS', 'key', '1', '2'] ); }); diff --git a/packages/bloom/lib/commands/bloom/MEXISTS.ts b/packages/bloom/lib/commands/bloom/MEXISTS.ts index a23b713b50c..acd85786f97 100644 --- a/packages/bloom/lib/commands/bloom/MEXISTS.ts +++ b/packages/bloom/lib/commands/bloom/MEXISTS.ts @@ -1,12 +1,14 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; -import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, items: RedisVariadicArgument) { - return pushVariadicArguments(['BF.MEXISTS', key], items); + parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { + parser.push('BF.MEXISTS'); + parser.pushKey(key); + parser.pushVariadic(items); }, transformReply: transformBooleanArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/RESERVE.spec.ts b/packages/bloom/lib/commands/bloom/RESERVE.spec.ts index caf40d4a48f..803577b350b 100644 --- a/packages/bloom/lib/commands/bloom/RESERVE.spec.ts +++ b/packages/bloom/lib/commands/bloom/RESERVE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import RESERVE from './RESERVE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.RESERVE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - RESERVE.transformArguments('key', 0.01, 100), + parseArgs(RESERVE, 'key', 0.01, 100), ['BF.RESERVE', 'key', '0.01', '100'] ); }); it('with EXPANSION', () => { assert.deepEqual( - RESERVE.transformArguments('key', 0.01, 100, { + parseArgs(RESERVE, 'key', 0.01, 100, { EXPANSION: 1 }), ['BF.RESERVE', 'key', '0.01', '100', 'EXPANSION', '1'] @@ -22,7 +23,7 @@ describe('BF.RESERVE', () => { it('with NONSCALING', () => { assert.deepEqual( - RESERVE.transformArguments('key', 0.01, 100, { + parseArgs(RESERVE, 'key', 0.01, 100, { NONSCALING: true }), ['BF.RESERVE', 'key', '0.01', '100', 'NONSCALING'] @@ -31,7 +32,7 @@ describe('BF.RESERVE', () => { it('with EXPANSION and NONSCALING', () => { assert.deepEqual( - RESERVE.transformArguments('key', 0.01, 100, { + parseArgs(RESERVE, 'key', 0.01, 100, { EXPANSION: 1, NONSCALING: true }), diff --git a/packages/bloom/lib/commands/bloom/RESERVE.ts b/packages/bloom/lib/commands/bloom/RESERVE.ts index 6bccb1d1d13..aff155ab769 100644 --- a/packages/bloom/lib/commands/bloom/RESERVE.ts +++ b/packages/bloom/lib/commands/bloom/RESERVE.ts @@ -1,4 +1,5 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export interface BfReserveOptions { EXPANSION?: number; @@ -6,25 +7,25 @@ export interface BfReserveOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, errorRate: number, capacity: number, options?: BfReserveOptions ) { - const args = ['BF.RESERVE', key, errorRate.toString(), capacity.toString()]; + parser.push('BF.RESERVE'); + parser.pushKey(key); + parser.push(errorRate.toString(), capacity.toString()); if (options?.EXPANSION) { - args.push('EXPANSION', options.EXPANSION.toString()); + parser.push('EXPANSION', options.EXPANSION.toString()); } if (options?.NONSCALING) { - args.push('NONSCALING'); + parser.push('NONSCALING'); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts b/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts index a7de98eabe7..a41a6e8e466 100644 --- a/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts +++ b/packages/bloom/lib/commands/bloom/SCANDUMP.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import SCANDUMP from './SCANDUMP'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('BF.SCANDUMP', () => { it('transformArguments', () => { assert.deepEqual( - SCANDUMP.transformArguments('key', 0), + parseArgs(SCANDUMP, 'key', 0), ['BF.SCANDUMP', 'key', '0'] ); }); diff --git a/packages/bloom/lib/commands/bloom/SCANDUMP.ts b/packages/bloom/lib/commands/bloom/SCANDUMP.ts index 588957b1743..37b1f57045c 100644 --- a/packages/bloom/lib/commands/bloom/SCANDUMP.ts +++ b/packages/bloom/lib/commands/bloom/SCANDUMP.ts @@ -1,10 +1,12 @@ -import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, iterator: number) { - return ['BF.SCANDUMP', key, iterator.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) { + parser.push('BF.SCANDUMP'); + parser.pushKey(key); + parser.push(iterator.toString()); }, transformReply(reply: UnwrapReply>) { return { diff --git a/packages/bloom/lib/commands/bloom/index.ts b/packages/bloom/lib/commands/bloom/index.ts index a93f79c9c56..e87f57220dd 100644 --- a/packages/bloom/lib/commands/bloom/index.ts +++ b/packages/bloom/lib/commands/bloom/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import type { RedisCommands, TypeMapping } from '@redis/client/lib/RESP/types'; import ADD from './ADD'; import CARD from './CARD'; diff --git a/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts index 1d2921cab75..44ccaf6046d 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INCRBY.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INCRBY from './INCRBY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CMS.INCRBY', () => { describe('transformArguments', () => { it('single item', () => { assert.deepEqual( - INCRBY.transformArguments('key', { + parseArgs(INCRBY, 'key', { item: 'item', incrementBy: 1 }), @@ -16,7 +17,7 @@ describe('CMS.INCRBY', () => { it('multiple items', () => { assert.deepEqual( - INCRBY.transformArguments('key', [{ + parseArgs(INCRBY, 'key', [{ item: 'a', incrementBy: 1 }, { diff --git a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts index 1dfbabbaa49..39cc52d31de 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts @@ -1,4 +1,5 @@ -import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; export interface BfIncrByItem { item: RedisArgument; @@ -6,27 +7,26 @@ export interface BfIncrByItem { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, items: BfIncrByItem | Array ) { - const args = ['CMS.INCRBY', key]; + parser.push('CMS.INCRBY'); + parser.pushKey(key); if (Array.isArray(items)) { for (const item of items) { - pushIncrByItem(args, item); + pushIncrByItem(parser, item); } } else { - pushIncrByItem(args, items); + pushIncrByItem(parser, items); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; -function pushIncrByItem(args: Array, { item, incrementBy }: BfIncrByItem): void { - args.push(item, incrementBy.toString()); +function pushIncrByItem(parser: CommandParser, { item, incrementBy }: BfIncrByItem): void { + parser.push(item, incrementBy.toString()); } diff --git a/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts index e650d78d2ed..cbc8065016a 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INFO.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INFO from './INFO'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CMS.INFO', () => { it('transformArguments', () => { assert.deepEqual( - INFO.transformArguments('key'), + parseArgs(INFO, 'key'), ['CMS.INFO', 'key'] ); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/INFO.ts b/packages/bloom/lib/commands/count-min-sketch/INFO.ts index e4aae5bf47b..9b77409f2d1 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INFO.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INFO.ts @@ -1,4 +1,5 @@ -import { RedisArgument, TuplesToMapReply, NumberReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, TuplesToMapReply, NumberReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; import { transformInfoV2Reply } from '../bloom'; export type CmsInfoReplyMap = TuplesToMapReply<[ @@ -14,10 +15,10 @@ export interface CmsInfoReply { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['CMS.INFO', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('CMS.INFO'); + parser.pushKey(key); }, transformReply: { 2: (reply: UnwrapReply>, _, typeMapping?: TypeMapping): CmsInfoReply => { diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts index a3d27c17df3..9fa1652a2e8 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INITBYDIM from './INITBYDIM'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CMS.INITBYDIM', () => { it('transformArguments', () => { assert.deepEqual( - INITBYDIM.transformArguments('key', 1000, 5), + parseArgs(INITBYDIM, 'key', 1000, 5), ['CMS.INITBYDIM', 'key', '1000', '5'] ); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts index 60790d421e4..cd295d0696e 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts @@ -1,10 +1,12 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, width: number, depth: number) { - return ['CMS.INITBYDIM', key, width.toString(), depth.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, width: number, depth: number) { + parser.push('CMS.INITBYDIM'); + parser.pushKey(key); + parser.push(width.toString(), depth.toString()); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts index 8df62020e89..b59bc14494f 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INITBYPROB from './INITBYPROB'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CMS.INITBYPROB', () => { it('transformArguments', () => { assert.deepEqual( - INITBYPROB.transformArguments('key', 0.001, 0.01), + parseArgs(INITBYPROB, 'key', 0.001, 0.01), ['CMS.INITBYPROB', 'key', '0.001', '0.01'] ); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts index 7b21755f17d..e7e85d4100b 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts @@ -1,10 +1,12 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, error: number, probability: number) { - return ['CMS.INITBYPROB', key, error.toString(), probability.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, error: number, probability: number) { + parser.push('CMS.INITBYPROB'); + parser.pushKey(key); + parser.push(error.toString(), probability.toString()); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts b/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts index eef4bd403ae..03e3d5c6364 100644 --- a/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import MERGE from './MERGE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CMS.MERGE', () => { describe('transformArguments', () => { it('without WEIGHTS', () => { assert.deepEqual( - MERGE.transformArguments('destination', ['source']), + parseArgs(MERGE, 'destination', ['source']), ['CMS.MERGE', 'destination', '1', 'source'] ); }); it('with WEIGHTS', () => { assert.deepEqual( - MERGE.transformArguments('destination', [{ + parseArgs(MERGE, 'destination', [{ name: 'source', weight: 1 }]), diff --git a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts index 2e63065d1cc..cc0fc929070 100644 --- a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts +++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts @@ -1,4 +1,5 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; interface BfMergeSketch { name: RedisArgument; @@ -8,26 +9,27 @@ interface BfMergeSketch { export type BfMergeSketches = Array | Array; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, destination: RedisArgument, source: BfMergeSketches ) { - let args = ['CMS.MERGE', destination, source.length.toString()]; + parser.push('CMS.MERGE'); + parser.pushKey(destination); + parser.push(source.length.toString()); if (isPlainSketches(source)) { - args = args.concat(source); + parser.pushVariadic(source); } else { - const { length } = args; - args[length + source.length] = 'WEIGHTS'; for (let i = 0; i < source.length; i++) { - args[length + i] = source[i].name; - args[length + source.length + i + 1] = source[i].weight.toString(); + parser.push(source[i].name); + } + parser.push('WEIGHTS'); + for (let i = 0; i < source.length; i++) { + parser.push(source[i].weight.toString()) } } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts b/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts index cc9c913b563..e12a519e962 100644 --- a/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts +++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import QUERY from './QUERY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CMS.QUERY', () => { it('transformArguments', () => { assert.deepEqual( - QUERY.transformArguments('key', 'item'), + parseArgs(QUERY, 'key', 'item'), ['CMS.QUERY', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts index 5d2905300b1..54d838c1935 100644 --- a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts @@ -1,11 +1,13 @@ -import { ArrayReply, NumberReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { ArrayReply, NumberReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, items: RedisVariadicArgument) { - return pushVariadicArguments(['CMS.QUERY', key], items); + parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { + parser.push('CMS.QUERY'); + parser.pushKey(key); + parser.pushVariadic(items); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/count-min-sketch/index.ts b/packages/bloom/lib/commands/count-min-sketch/index.ts index 4f0f395ca3d..1132a7524e1 100644 --- a/packages/bloom/lib/commands/count-min-sketch/index.ts +++ b/packages/bloom/lib/commands/count-min-sketch/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import type { RedisCommands } from '@redis/client/lib/RESP/types'; import INCRBY from './INCRBY'; import INFO from './INFO'; import INITBYDIM from './INITBYDIM'; diff --git a/packages/bloom/lib/commands/cuckoo/ADD.spec.ts b/packages/bloom/lib/commands/cuckoo/ADD.spec.ts index fa610cc6666..7fa518fea84 100644 --- a/packages/bloom/lib/commands/cuckoo/ADD.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/ADD.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import ADD from './ADD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.ADD', () => { it('transformArguments', () => { assert.deepEqual( - ADD.transformArguments('key', 'item'), + parseArgs(ADD, 'key', 'item'), ['CF.ADD', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/cuckoo/ADD.ts b/packages/bloom/lib/commands/cuckoo/ADD.ts index 52e98a801d4..3d0dc77be2d 100644 --- a/packages/bloom/lib/commands/cuckoo/ADD.ts +++ b/packages/bloom/lib/commands/cuckoo/ADD.ts @@ -1,11 +1,13 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, item: RedisArgument) { - return ['CF.ADD', key, item]; + parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { + parser.push('CF.ADD'); + parser.pushKey(key); + parser.push(item); }, transformReply: transformBooleanReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts b/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts index f50ad87dc15..c142733ce40 100644 --- a/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/ADDNX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import ADDNX from './ADDNX'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.ADDNX', () => { it('transformArguments', () => { assert.deepEqual( - ADDNX.transformArguments('key', 'item'), + parseArgs(ADDNX, 'key', 'item'), ['CF.ADDNX', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/cuckoo/ADDNX.ts b/packages/bloom/lib/commands/cuckoo/ADDNX.ts index c739077ee46..f358f1581ec 100644 --- a/packages/bloom/lib/commands/cuckoo/ADDNX.ts +++ b/packages/bloom/lib/commands/cuckoo/ADDNX.ts @@ -1,11 +1,13 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, item: RedisArgument) { - return ['CF.ADDNX', key, item]; + parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { + parser.push('CF.ADDNX'); + parser.pushKey(key); + parser.push(item); }, transformReply: transformBooleanReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts b/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts index ff8d40f064e..9393494d852 100644 --- a/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/COUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import COUNT from './COUNT'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.COUNT', () => { it('transformArguments', () => { assert.deepEqual( - COUNT.transformArguments('key', 'item'), + parseArgs(COUNT, 'key', 'item'), ['CF.COUNT', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/cuckoo/COUNT.ts b/packages/bloom/lib/commands/cuckoo/COUNT.ts index 2284edff174..512b0143272 100644 --- a/packages/bloom/lib/commands/cuckoo/COUNT.ts +++ b/packages/bloom/lib/commands/cuckoo/COUNT.ts @@ -1,10 +1,12 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, item: RedisArgument) { - return ['CF.COUNT', key, item]; + parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { + parser.push('CF.COUNT'); + parser.pushKey(key); + parser.push(item); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/DEL.spec.ts b/packages/bloom/lib/commands/cuckoo/DEL.spec.ts index e02b5636e12..41ed653bfc9 100644 --- a/packages/bloom/lib/commands/cuckoo/DEL.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/DEL.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import DEL from './DEL'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.DEL', () => { it('transformArguments', () => { assert.deepEqual( - DEL.transformArguments('key', 'item'), + parseArgs(DEL, 'key', 'item'), ['CF.DEL', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/cuckoo/DEL.ts b/packages/bloom/lib/commands/cuckoo/DEL.ts index 0af8ebc851b..0b2bdaea990 100644 --- a/packages/bloom/lib/commands/cuckoo/DEL.ts +++ b/packages/bloom/lib/commands/cuckoo/DEL.ts @@ -1,11 +1,13 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, item: RedisArgument) { - return ['CF.DEL', key, item]; + parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { + parser.push('CF.DEL'); + parser.pushKey(key); + parser.push(item); }, transformReply: transformBooleanReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts b/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts index 899c11e8394..f77a9d69eff 100644 --- a/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/EXISTS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import EXISTS from './EXISTS'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.EXISTS', () => { it('transformArguments', () => { assert.deepEqual( - EXISTS.transformArguments('key', 'item'), + parseArgs(EXISTS, 'key', 'item'), ['CF.EXISTS', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/cuckoo/EXISTS.ts b/packages/bloom/lib/commands/cuckoo/EXISTS.ts index 8fd74ca47ca..ef93462990b 100644 --- a/packages/bloom/lib/commands/cuckoo/EXISTS.ts +++ b/packages/bloom/lib/commands/cuckoo/EXISTS.ts @@ -1,11 +1,13 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, item: RedisArgument) { - return ['CF.EXISTS', key, item]; + parseCommand(parser: CommandParser, key: RedisArgument, item: RedisArgument) { + parser.push('CF.EXISTS'); + parser.pushKey(key); + parser.push(item); }, transformReply: transformBooleanReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/INFO.spec.ts b/packages/bloom/lib/commands/cuckoo/INFO.spec.ts index 222177c4650..c5503ed113b 100644 --- a/packages/bloom/lib/commands/cuckoo/INFO.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/INFO.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INFO from './INFO'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.INFO', () => { it('transformArguments', () => { assert.deepEqual( - INFO.transformArguments('cuckoo'), + parseArgs(INFO, 'cuckoo'), ['CF.INFO', 'cuckoo'] ); }); diff --git a/packages/bloom/lib/commands/cuckoo/INFO.ts b/packages/bloom/lib/commands/cuckoo/INFO.ts index 70a7d80c6f2..15972b206ff 100644 --- a/packages/bloom/lib/commands/cuckoo/INFO.ts +++ b/packages/bloom/lib/commands/cuckoo/INFO.ts @@ -1,4 +1,5 @@ -import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; import { transformInfoV2Reply } from '../bloom'; export type CfInfoReplyMap = TuplesToMapReply<[ @@ -13,10 +14,10 @@ export type CfInfoReplyMap = TuplesToMapReply<[ ]>; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['CF.INFO', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('CF.INFO'); + parser.pushKey(key); }, transformReply: { 2: (reply: UnwrapReply>, _, typeMapping?: TypeMapping): CfInfoReplyMap => { diff --git a/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts b/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts index 096cf547098..dc2bd574517 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INSERT from './INSERT'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.INSERT', () => { it('transformArguments', () => { assert.deepEqual( - INSERT.transformArguments('key', 'item', { + parseArgs(INSERT, 'key', 'item', { CAPACITY: 100, NOCREATE: true }), diff --git a/packages/bloom/lib/commands/cuckoo/INSERT.ts b/packages/bloom/lib/commands/cuckoo/INSERT.ts index d6df64eea1a..75534e0a7fa 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERT.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERT.ts @@ -1,34 +1,37 @@ -import { Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments, transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, RedisArgument } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; export interface CfInsertOptions { CAPACITY?: number; NOCREATE?: boolean; } -export function transofrmCfInsertArguments( - command: RedisArgument, +export function parseCfInsertArguments( + parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument, options?: CfInsertOptions ) { - const args = [command, key]; + parser.pushKey(key); if (options?.CAPACITY !== undefined) { - args.push('CAPACITY', options.CAPACITY.toString()); + parser.push('CAPACITY', options.CAPACITY.toString()); } if (options?.NOCREATE) { - args.push('NOCREATE'); + parser.push('NOCREATE'); } - args.push('ITEMS'); - return pushVariadicArguments(args, items); + parser.push('ITEMS'); + parser.pushVariadic(items); } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments: transofrmCfInsertArguments.bind(undefined, 'CF.INSERT'), + parseCommand(...args: Parameters) { + args[0].push('CF.INSERT'); + parseCfInsertArguments(...args); + }, transformReply: transformBooleanArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts b/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts index 0f874278220..648d9be7ac8 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INSERTNX from './INSERTNX'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.INSERTNX', () => { it('transformArguments', () => { assert.deepEqual( - INSERTNX.transformArguments('key', 'item', { + parseArgs(INSERTNX, 'key', 'item', { CAPACITY: 100, NOCREATE: true }), diff --git a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts index 5cd56e794f9..581cfcd9e60 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts @@ -1,9 +1,11 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; -import INSERT, { transofrmCfInsertArguments } from './INSERT'; +import { Command } from '@redis/client/lib/RESP/types'; +import INSERT, { parseCfInsertArguments } from './INSERT'; export default { - FIRST_KEY_INDEX: INSERT.FIRST_KEY_INDEX, IS_READ_ONLY: INSERT.IS_READ_ONLY, - transformArguments: transofrmCfInsertArguments.bind(undefined, 'CF.INSERTNX'), + parseCommand(...args: Parameters) { + args[0].push('CF.INSERTNX'); + parseCfInsertArguments(...args); + }, transformReply: INSERT.transformReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts index 5b880e0dd9d..5415c787dda 100644 --- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import LOADCHUNK from './LOADCHUNK'; import { RESP_TYPES } from '@redis/client'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.LOADCHUNK', () => { it('transformArguments', () => { assert.deepEqual( - LOADCHUNK.transformArguments('item', 0, ''), + parseArgs(LOADCHUNK, 'item', 0, ''), ['CF.LOADCHUNK', 'item', '0', ''] ); }); diff --git a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts index 08cb749b595..420774a6504 100644 --- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts @@ -1,10 +1,12 @@ -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, iterator: number, chunk: RedisArgument) { - return ['CF.LOADCHUNK', key, iterator.toString(), chunk]; + parseCommand(parser: CommandParser, key: RedisArgument, iterator: number, chunk: RedisArgument) { + parser.push('CF.LOADCHUNK'); + parser.pushKey(key); + parser.push(iterator.toString(), chunk); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts b/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts index b8f2556bc4f..53546e4156e 100644 --- a/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/RESERVE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import RESERVE from './RESERVE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.RESERVE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - RESERVE.transformArguments('key', 4), + parseArgs(RESERVE, 'key', 4), ['CF.RESERVE', 'key', '4'] ); }); it('with EXPANSION', () => { assert.deepEqual( - RESERVE.transformArguments('key', 4, { + parseArgs(RESERVE, 'key', 4, { EXPANSION: 1 }), ['CF.RESERVE', 'key', '4', 'EXPANSION', '1'] @@ -22,7 +23,7 @@ describe('CF.RESERVE', () => { it('with BUCKETSIZE', () => { assert.deepEqual( - RESERVE.transformArguments('key', 4, { + parseArgs(RESERVE, 'key', 4, { BUCKETSIZE: 2 }), ['CF.RESERVE', 'key', '4', 'BUCKETSIZE', '2'] @@ -31,7 +32,7 @@ describe('CF.RESERVE', () => { it('with MAXITERATIONS', () => { assert.deepEqual( - RESERVE.transformArguments('key', 4, { + parseArgs(RESERVE, 'key', 4, { MAXITERATIONS: 1 }), ['CF.RESERVE', 'key', '4', 'MAXITERATIONS', '1'] diff --git a/packages/bloom/lib/commands/cuckoo/RESERVE.ts b/packages/bloom/lib/commands/cuckoo/RESERVE.ts index bc80daa0087..ba401dcdee9 100644 --- a/packages/bloom/lib/commands/cuckoo/RESERVE.ts +++ b/packages/bloom/lib/commands/cuckoo/RESERVE.ts @@ -1,4 +1,5 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export interface CfReserveOptions { BUCKETSIZE?: number; @@ -7,28 +8,28 @@ export interface CfReserveOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, capacity: number, options?: CfReserveOptions ) { - const args = ['CF.RESERVE', key, capacity.toString()]; + parser.push('CF.RESERVE'); + parser.pushKey(key); + parser.push(capacity.toString()); if (options?.BUCKETSIZE !== undefined) { - args.push('BUCKETSIZE', options.BUCKETSIZE.toString()); + parser.push('BUCKETSIZE', options.BUCKETSIZE.toString()); } if (options?.MAXITERATIONS !== undefined) { - args.push('MAXITERATIONS', options.MAXITERATIONS.toString()); + parser.push('MAXITERATIONS', options.MAXITERATIONS.toString()); } if (options?.EXPANSION !== undefined) { - args.push('EXPANSION', options.EXPANSION.toString()); + parser.push('EXPANSION', options.EXPANSION.toString()); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts b/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts index e1bac59d323..60a57ac46ab 100644 --- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts +++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import SCANDUMP from './SCANDUMP'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('CF.SCANDUMP', () => { it('transformArguments', () => { assert.deepEqual( - SCANDUMP.transformArguments('key', 0), + parseArgs(SCANDUMP, 'key', 0), ['CF.SCANDUMP', 'key', '0'] ); }); diff --git a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts index dc076689288..aab7a5eecc2 100644 --- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts +++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts @@ -1,10 +1,12 @@ -import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, iterator: number) { - return ['CF.SCANDUMP', key, iterator.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, iterator: number) { + parser.push('CF.SCANDUMP'); + parser.pushKey(key); + parser.push(iterator.toString()); }, transformReply(reply: UnwrapReply>) { return { diff --git a/packages/bloom/lib/commands/cuckoo/index.ts b/packages/bloom/lib/commands/cuckoo/index.ts index 62c63fe8d19..7ee99b41cd3 100644 --- a/packages/bloom/lib/commands/cuckoo/index.ts +++ b/packages/bloom/lib/commands/cuckoo/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import type { RedisCommands } from '@redis/client/lib/RESP/types'; import ADD from './ADD'; import ADDNX from './ADDNX'; import COUNT from './COUNT'; diff --git a/packages/bloom/lib/commands/t-digest/ADD.spec.ts b/packages/bloom/lib/commands/t-digest/ADD.spec.ts index 31d4957c6ad..7578fb9378b 100644 --- a/packages/bloom/lib/commands/t-digest/ADD.spec.ts +++ b/packages/bloom/lib/commands/t-digest/ADD.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import ADD from './ADD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.ADD', () => { it('transformArguments', () => { assert.deepEqual( - ADD.transformArguments('key', [1, 2]), + parseArgs(ADD, 'key', [1, 2]), ['TDIGEST.ADD', 'key', '1', '2'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/ADD.ts b/packages/bloom/lib/commands/t-digest/ADD.ts index e7c6d7c4429..d9d67144a95 100644 --- a/packages/bloom/lib/commands/t-digest/ADD.ts +++ b/packages/bloom/lib/commands/t-digest/ADD.ts @@ -1,16 +1,15 @@ -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, values: Array) { - const args = ['TDIGEST.ADD', key]; + parseCommand(parser: CommandParser, key: RedisArgument, values: Array) { + parser.push('TDIGEST.ADD'); + parser.pushKey(key); for (const value of values) { - args.push(value.toString()); + parser.push(value.toString()); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts b/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts index a6443d77432..81a2c75dff5 100644 --- a/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/BYRANK.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import BYRANK from './BYRANK'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.BYRANK', () => { it('transformArguments', () => { assert.deepEqual( - BYRANK.transformArguments('key', [1, 2]), + parseArgs(BYRANK, 'key', [1, 2]), ['TDIGEST.BYRANK', 'key', '1', '2'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/BYRANK.ts b/packages/bloom/lib/commands/t-digest/BYRANK.ts index 8b48acd1b1b..126c9963e71 100644 --- a/packages/bloom/lib/commands/t-digest/BYRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYRANK.ts @@ -1,24 +1,25 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformDoubleArrayReply } from '@redis/client/lib/commands/generic-transformers'; export function transformByRankArguments( - command: RedisArgument, + parser: CommandParser, key: RedisArgument, ranks: Array ) { - const args = [command, key]; + parser.pushKey(key); for (const rank of ranks) { - args.push(rank.toString()); + parser.push(rank.toString()); } - - return args; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments: transformByRankArguments.bind(undefined, 'TDIGEST.BYRANK'), + parseCommand(...args: Parameters) { + args[0].push('TDIGEST.BYRANK'); + transformByRankArguments(...args); + }, transformReply: transformDoubleArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts b/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts index f5bb4e62816..c8f794bef57 100644 --- a/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import BYREVRANK from './BYREVRANK'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.BYREVRANK', () => { it('transformArguments', () => { assert.deepEqual( - BYREVRANK.transformArguments('key', [1, 2]), + parseArgs(BYREVRANK, 'key', [1, 2]), ['TDIGEST.BYREVRANK', 'key', '1', '2'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts index 9f62b42d812..4995bf86cc0 100644 --- a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts @@ -1,9 +1,11 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; +import { Command } from '@redis/client/lib/RESP/types'; import BYRANK, { transformByRankArguments } from './BYRANK'; export default { - FIRST_KEY_INDEX: BYRANK.FIRST_KEY_INDEX, IS_READ_ONLY: BYRANK.IS_READ_ONLY, - transformArguments: transformByRankArguments.bind(undefined, 'TDIGEST.BYREVRANK'), + parseCommand(...args: Parameters) { + args[0].push('TDIGEST.BYREVRANK'); + transformByRankArguments(...args); + }, transformReply: BYRANK.transformReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/CDF.spec.ts b/packages/bloom/lib/commands/t-digest/CDF.spec.ts index 09208deba11..2689bf2fc9a 100644 --- a/packages/bloom/lib/commands/t-digest/CDF.spec.ts +++ b/packages/bloom/lib/commands/t-digest/CDF.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import CDF from './CDF'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.CDF', () => { it('transformArguments', () => { assert.deepEqual( - CDF.transformArguments('key', [1, 2]), + parseArgs(CDF, 'key', [1, 2]), ['TDIGEST.CDF', 'key', '1', '2'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/CDF.ts b/packages/bloom/lib/commands/t-digest/CDF.ts index 0fbdedb3a47..e786dc4c8a8 100644 --- a/packages/bloom/lib/commands/t-digest/CDF.ts +++ b/packages/bloom/lib/commands/t-digest/CDF.ts @@ -1,17 +1,16 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformDoubleArrayReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, values: Array) { - const args = ['TDIGEST.CDF', key]; + parseCommand(parser: CommandParser, key: RedisArgument, values: Array) { + parser.push('TDIGEST.CDF'); + parser.pushKey(key); for (const item of values) { - args.push(item.toString()); + parser.push(item.toString()); } - - return args; }, transformReply: transformDoubleArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/CREATE.spec.ts b/packages/bloom/lib/commands/t-digest/CREATE.spec.ts index 781b2a7e432..0f218e07ab8 100644 --- a/packages/bloom/lib/commands/t-digest/CREATE.spec.ts +++ b/packages/bloom/lib/commands/t-digest/CREATE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import CREATE from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.CREATE', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - CREATE.transformArguments('key'), + parseArgs(CREATE, 'key'), ['TDIGEST.CREATE', 'key'] ); }); it('with COMPRESSION', () => { assert.deepEqual( - CREATE.transformArguments('key', { + parseArgs(CREATE, 'key', { COMPRESSION: 100 }), ['TDIGEST.CREATE', 'key', 'COMPRESSION', '100'] diff --git a/packages/bloom/lib/commands/t-digest/CREATE.ts b/packages/bloom/lib/commands/t-digest/CREATE.ts index 8b832487daa..fd160cce842 100644 --- a/packages/bloom/lib/commands/t-digest/CREATE.ts +++ b/packages/bloom/lib/commands/t-digest/CREATE.ts @@ -1,20 +1,19 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export interface TDigestCreateOptions { COMPRESSION?: number; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: TDigestCreateOptions) { - const args = ['TDIGEST.CREATE', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: TDigestCreateOptions) { + parser.push('TDIGEST.CREATE'); + parser.pushKey(key); if (options?.COMPRESSION !== undefined) { - args.push('COMPRESSION', options.COMPRESSION.toString()); + parser.push('COMPRESSION', options.COMPRESSION.toString()); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/INFO.spec.ts b/packages/bloom/lib/commands/t-digest/INFO.spec.ts index 247f4ab0b61..d5b8b3e13ed 100644 --- a/packages/bloom/lib/commands/t-digest/INFO.spec.ts +++ b/packages/bloom/lib/commands/t-digest/INFO.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INFO from './INFO'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.INFO', () => { it('transformArguments', () => { assert.deepEqual( - INFO.transformArguments('key'), + parseArgs(INFO, 'key'), ['TDIGEST.INFO', 'key'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/INFO.ts b/packages/bloom/lib/commands/t-digest/INFO.ts index c7c2357d2b4..43624e8f9b9 100644 --- a/packages/bloom/lib/commands/t-digest/INFO.ts +++ b/packages/bloom/lib/commands/t-digest/INFO.ts @@ -1,4 +1,5 @@ -import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; import { transformInfoV2Reply } from '../bloom'; export type TdInfoReplyMap = TuplesToMapReply<[ @@ -14,10 +15,10 @@ export type TdInfoReplyMap = TuplesToMapReply<[ ]>; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['TDIGEST.INFO', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TDIGEST.INFO'); + parser.pushKey(key); }, transformReply: { 2: (reply: UnwrapReply>, _, typeMapping?: TypeMapping): TdInfoReplyMap => { diff --git a/packages/bloom/lib/commands/t-digest/MAX.spec.ts b/packages/bloom/lib/commands/t-digest/MAX.spec.ts index caa92b0a6a0..920c9d11391 100644 --- a/packages/bloom/lib/commands/t-digest/MAX.spec.ts +++ b/packages/bloom/lib/commands/t-digest/MAX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import MAX from './MAX'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.MAX', () => { it('transformArguments', () => { assert.deepEqual( - MAX.transformArguments('key'), + parseArgs(MAX, 'key'), ['TDIGEST.MAX', 'key'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/MAX.ts b/packages/bloom/lib/commands/t-digest/MAX.ts index ef778f832a6..64852d8e6dc 100644 --- a/packages/bloom/lib/commands/t-digest/MAX.ts +++ b/packages/bloom/lib/commands/t-digest/MAX.ts @@ -1,11 +1,12 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['TDIGEST.MAX', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TDIGEST.MAX'); + parser.pushKey(key); }, transformReply: transformDoubleReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/MERGE.spec.ts b/packages/bloom/lib/commands/t-digest/MERGE.spec.ts index 1ee792e3a40..f2a7c1a1192 100644 --- a/packages/bloom/lib/commands/t-digest/MERGE.spec.ts +++ b/packages/bloom/lib/commands/t-digest/MERGE.spec.ts @@ -1,20 +1,21 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import MERGE from './MERGE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.MERGE', () => { describe('transformArguments', () => { describe('source', () => { it('string', () => { assert.deepEqual( - MERGE.transformArguments('destination', 'source'), + parseArgs(MERGE, 'destination', 'source'), ['TDIGEST.MERGE', 'destination', '1', 'source'] ); }); it('Array', () => { assert.deepEqual( - MERGE.transformArguments('destination', ['1', '2']), + parseArgs(MERGE, 'destination', ['1', '2']), ['TDIGEST.MERGE', 'destination', '2', '1', '2'] ); }); @@ -22,7 +23,7 @@ describe('TDIGEST.MERGE', () => { it('with COMPRESSION', () => { assert.deepEqual( - MERGE.transformArguments('destination', 'source', { + parseArgs(MERGE, 'destination', 'source', { COMPRESSION: 100 }), ['TDIGEST.MERGE', 'destination', '1', 'source', 'COMPRESSION', '100'] @@ -31,7 +32,7 @@ describe('TDIGEST.MERGE', () => { it('with OVERRIDE', () => { assert.deepEqual( - MERGE.transformArguments('destination', 'source', { + parseArgs(MERGE, 'destination', 'source', { OVERRIDE: true }), ['TDIGEST.MERGE', 'destination', '1', 'source', 'OVERRIDE'] diff --git a/packages/bloom/lib/commands/t-digest/MERGE.ts b/packages/bloom/lib/commands/t-digest/MERGE.ts index e9cd99aabf9..1031f0e9170 100644 --- a/packages/bloom/lib/commands/t-digest/MERGE.ts +++ b/packages/bloom/lib/commands/t-digest/MERGE.ts @@ -1,5 +1,6 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export interface TDigestMergeOptions { COMPRESSION?: number; @@ -7,24 +8,24 @@ export interface TDigestMergeOptions { } export default { - FIRST_KEY_INDEX: undefined, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, destination: RedisArgument, source: RedisVariadicArgument, options?: TDigestMergeOptions ) { - const args = pushVariadicArgument(['TDIGEST.MERGE', destination], source); + parser.push('TDIGEST.MERGE'); + parser.pushKey(destination); + parser.pushKeysLength(source); if (options?.COMPRESSION !== undefined) { - args.push('COMPRESSION', options.COMPRESSION.toString()); + parser.push('COMPRESSION', options.COMPRESSION.toString()); } if (options?.OVERRIDE) { - args.push('OVERRIDE'); + parser.push('OVERRIDE'); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/MIN.spec.ts b/packages/bloom/lib/commands/t-digest/MIN.spec.ts index 0d1637cc9b7..278248ea465 100644 --- a/packages/bloom/lib/commands/t-digest/MIN.spec.ts +++ b/packages/bloom/lib/commands/t-digest/MIN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import MIN from './MIN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.MIN', () => { it('transformArguments', () => { assert.deepEqual( - MIN.transformArguments('key'), + parseArgs(MIN, 'key'), ['TDIGEST.MIN', 'key'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/MIN.ts b/packages/bloom/lib/commands/t-digest/MIN.ts index 914998a1ec4..cc44dbbea4e 100644 --- a/packages/bloom/lib/commands/t-digest/MIN.ts +++ b/packages/bloom/lib/commands/t-digest/MIN.ts @@ -1,11 +1,12 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['TDIGEST.MIN', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TDIGEST.MIN'); + parser.pushKey(key); }, transformReply: transformDoubleReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts b/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts index c427f8c4501..ac7249d12d9 100644 --- a/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts +++ b/packages/bloom/lib/commands/t-digest/QUANTILE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import QUANTILE from './QUANTILE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.QUANTILE', () => { it('transformArguments', () => { assert.deepEqual( - QUANTILE.transformArguments('key', [1, 2]), + parseArgs(QUANTILE, 'key', [1, 2]), ['TDIGEST.QUANTILE', 'key', '1', '2'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/QUANTILE.ts b/packages/bloom/lib/commands/t-digest/QUANTILE.ts index f7057a37d1c..962c3a9b4ca 100644 --- a/packages/bloom/lib/commands/t-digest/QUANTILE.ts +++ b/packages/bloom/lib/commands/t-digest/QUANTILE.ts @@ -1,17 +1,16 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformDoubleArrayReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, quantiles: Array) { - const args = ['TDIGEST.QUANTILE', key]; + parseCommand(parser: CommandParser, key: RedisArgument, quantiles: Array) { + parser.push('TDIGEST.QUANTILE'); + parser.pushKey(key); for (const quantile of quantiles) { - args.push(quantile.toString()); + parser.push(quantile.toString()); } - - return args; }, transformReply: transformDoubleArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/RANK.spec.ts b/packages/bloom/lib/commands/t-digest/RANK.spec.ts index dcdae48cb06..f1747662f0a 100644 --- a/packages/bloom/lib/commands/t-digest/RANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/RANK.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import RANK from './RANK'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.RANK', () => { it('transformArguments', () => { assert.deepEqual( - RANK.transformArguments('key', [1, 2]), + parseArgs(RANK, 'key', [1, 2]), ['TDIGEST.RANK', 'key', '1', '2'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/RANK.ts b/packages/bloom/lib/commands/t-digest/RANK.ts index 8c18ad12778..316ca74b542 100644 --- a/packages/bloom/lib/commands/t-digest/RANK.ts +++ b/packages/bloom/lib/commands/t-digest/RANK.ts @@ -1,22 +1,23 @@ -import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; export function transformRankArguments( - command: RedisArgument, + parser: CommandParser, key: RedisArgument, values: Array ) { - const args = [command, key]; + parser.pushKey(key); for (const value of values) { - args.push(value.toString()); + parser.push(value.toString()); } - - return args; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments: transformRankArguments.bind(undefined, 'TDIGEST.RANK'), + parseCommand(...args: Parameters) { + args[0].push('TDIGEST.RANK'); + transformRankArguments(...args); + }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/RESET.spec.ts b/packages/bloom/lib/commands/t-digest/RESET.spec.ts index 072257113b9..8e1fc12e6e3 100644 --- a/packages/bloom/lib/commands/t-digest/RESET.spec.ts +++ b/packages/bloom/lib/commands/t-digest/RESET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import RESET from './RESET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.RESET', () => { it('transformArguments', () => { assert.deepEqual( - RESET.transformArguments('key'), + parseArgs(RESET, 'key'), ['TDIGEST.RESET', 'key'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/RESET.ts b/packages/bloom/lib/commands/t-digest/RESET.ts index 372a1efd488..571102bfc20 100644 --- a/packages/bloom/lib/commands/t-digest/RESET.ts +++ b/packages/bloom/lib/commands/t-digest/RESET.ts @@ -1,10 +1,11 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument) { - return ['TDIGEST.RESET', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TDIGEST.RESET'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts b/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts index baa1b94afa8..be7b23b2238 100644 --- a/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts +++ b/packages/bloom/lib/commands/t-digest/REVRANK.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import REVRANK from './REVRANK'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.REVRANK', () => { it('transformArguments', () => { assert.deepEqual( - REVRANK.transformArguments('key', [1, 2]), + parseArgs(REVRANK, 'key', [1, 2]), ['TDIGEST.REVRANK', 'key', '1', '2'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/REVRANK.ts b/packages/bloom/lib/commands/t-digest/REVRANK.ts index 456b2be5a38..ca9301bb423 100644 --- a/packages/bloom/lib/commands/t-digest/REVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/REVRANK.ts @@ -1,9 +1,11 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; +import { Command } from '@redis/client/lib/RESP/types'; import RANK, { transformRankArguments } from './RANK'; export default { - FIRST_KEY_INDEX: RANK.FIRST_KEY_INDEX, IS_READ_ONLY: RANK.IS_READ_ONLY, - transformArguments: transformRankArguments.bind(undefined, 'TDIGEST.REVRANK'), + parseCommand(...args: Parameters) { + args[0].push('TDIGEST.REVRANK'); + transformRankArguments(...args); + }, transformReply: RANK.transformReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts index c43c0f47553..8e83c736476 100644 --- a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts +++ b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import TRIMMED_MEAN from './TRIMMED_MEAN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TDIGEST.TRIMMED_MEAN', () => { it('transformArguments', () => { assert.deepEqual( - TRIMMED_MEAN.transformArguments('key', 0, 1), + parseArgs(TRIMMED_MEAN, 'key', 0, 1), ['TDIGEST.TRIMMED_MEAN', 'key', '0', '1'] ); }); diff --git a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts index f91dd7d8093..f411198260a 100644 --- a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts +++ b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts @@ -1,20 +1,18 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, lowCutPercentile: number, highCutPercentile: number ) { - return [ - 'TDIGEST.TRIMMED_MEAN', - key, - lowCutPercentile.toString(), - highCutPercentile.toString() - ]; + parser.push('TDIGEST.TRIMMED_MEAN'); + parser.pushKey(key); + parser.push(lowCutPercentile.toString(), highCutPercentile.toString()); }, transformReply: transformDoubleReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/t-digest/index.ts b/packages/bloom/lib/commands/t-digest/index.ts index d180911dbf9..fb80b35d0a1 100644 --- a/packages/bloom/lib/commands/t-digest/index.ts +++ b/packages/bloom/lib/commands/t-digest/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import type { RedisCommands } from '@redis/client/lib/RESP/types'; import ADD from './ADD'; import BYRANK from './BYRANK'; import BYREVRANK from './BYREVRANK'; diff --git a/packages/bloom/lib/commands/top-k/ADD.spec.ts b/packages/bloom/lib/commands/top-k/ADD.spec.ts index 8f6f9300b36..15a7a9ce1dd 100644 --- a/packages/bloom/lib/commands/top-k/ADD.spec.ts +++ b/packages/bloom/lib/commands/top-k/ADD.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import ADD from './ADD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TOPK.ADD', () => { it('transformArguments', () => { assert.deepEqual( - ADD.transformArguments('key', 'item'), + parseArgs(ADD, 'key', 'item'), ['TOPK.ADD', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/top-k/ADD.ts b/packages/bloom/lib/commands/top-k/ADD.ts index 99982cc8e64..42b162165c7 100644 --- a/packages/bloom/lib/commands/top-k/ADD.ts +++ b/packages/bloom/lib/commands/top-k/ADD.ts @@ -1,11 +1,13 @@ -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, items: RedisVariadicArgument) { - return pushVariadicArguments(['TOPK.ADD', key], items); + parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { + parser.push('TOPK.ADD'); + parser.pushKey(key); + parser.pushVariadic(items); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/COUNT.spec.ts b/packages/bloom/lib/commands/top-k/COUNT.spec.ts index dce03f0e78c..a242edfef8a 100644 --- a/packages/bloom/lib/commands/top-k/COUNT.spec.ts +++ b/packages/bloom/lib/commands/top-k/COUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import COUNT from './COUNT'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TOPK.COUNT', () => { it('transformArguments', () => { assert.deepEqual( - COUNT.transformArguments('key', 'item'), + parseArgs(COUNT, 'key', 'item'), ['TOPK.COUNT', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/top-k/COUNT.ts b/packages/bloom/lib/commands/top-k/COUNT.ts index 7e3ccc6dc4c..7cca009c599 100644 --- a/packages/bloom/lib/commands/top-k/COUNT.ts +++ b/packages/bloom/lib/commands/top-k/COUNT.ts @@ -1,11 +1,13 @@ -import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, items: RedisVariadicArgument) { - return pushVariadicArguments(['TOPK.COUNT', key], items); + parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { + parser.push('TOPK.COUNT'); + parser.pushKey(key); + parser.pushVariadic(items); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/INCRBY.spec.ts b/packages/bloom/lib/commands/top-k/INCRBY.spec.ts index aa7032a9a02..94e5b1d7058 100644 --- a/packages/bloom/lib/commands/top-k/INCRBY.spec.ts +++ b/packages/bloom/lib/commands/top-k/INCRBY.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INCRBY from './INCRBY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TOPK.INCRBY', () => { describe('transformArguments', () => { it('single item', () => { assert.deepEqual( - INCRBY.transformArguments('key', { + parseArgs(INCRBY, 'key', { item: 'item', incrementBy: 1 }), @@ -16,7 +17,7 @@ describe('TOPK.INCRBY', () => { it('multiple items', () => { assert.deepEqual( - INCRBY.transformArguments('key', [{ + parseArgs(INCRBY, 'key', [{ item: 'a', incrementBy: 1 }, { diff --git a/packages/bloom/lib/commands/top-k/INCRBY.ts b/packages/bloom/lib/commands/top-k/INCRBY.ts index 16decf44dca..52ea0edc968 100644 --- a/packages/bloom/lib/commands/top-k/INCRBY.ts +++ b/packages/bloom/lib/commands/top-k/INCRBY.ts @@ -1,32 +1,32 @@ -import { RedisArgument, ArrayReply, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, SimpleStringReply, NullReply, Command } from '@redis/client/lib/RESP/types'; export interface TopKIncrByItem { item: string; incrementBy: number; } -function pushIncrByItem(args: Array, { item, incrementBy }: TopKIncrByItem) { - args.push(item, incrementBy.toString()); +function pushIncrByItem(parser: CommandParser, { item, incrementBy }: TopKIncrByItem) { + parser.push(item, incrementBy.toString()); } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, items: TopKIncrByItem | Array ) { - const args = ['TOPK.INCRBY', key]; + parser.push('TOPK.INCRBY'); + parser.pushKey(key); if (Array.isArray(items)) { for (const item of items) { - pushIncrByItem(args, item); + pushIncrByItem(parser, item); } } else { - pushIncrByItem(args, items); + pushIncrByItem(parser, items); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/INFO.spec.ts b/packages/bloom/lib/commands/top-k/INFO.spec.ts index 8e17829a2a6..2efbf0bdbef 100644 --- a/packages/bloom/lib/commands/top-k/INFO.spec.ts +++ b/packages/bloom/lib/commands/top-k/INFO.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import INFO from './INFO'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TOPK INFO', () => { it('transformArguments', () => { assert.deepEqual( - INFO.transformArguments('key'), + parseArgs(INFO, 'key'), ['TOPK.INFO', 'key'] ); }); diff --git a/packages/bloom/lib/commands/top-k/INFO.ts b/packages/bloom/lib/commands/top-k/INFO.ts index e6f55ac2c1b..89ebeaa8ebe 100644 --- a/packages/bloom/lib/commands/top-k/INFO.ts +++ b/packages/bloom/lib/commands/top-k/INFO.ts @@ -1,5 +1,6 @@ -import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; -import { RedisArgument, TuplesToMapReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, TuplesToMapReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; import { transformInfoV2Reply } from '../bloom'; export type TopKInfoReplyMap = TuplesToMapReply<[ @@ -10,10 +11,10 @@ export type TopKInfoReplyMap = TuplesToMapReply<[ ]>; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['TOPK.INFO', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TOPK.INFO'); + parser.pushKey(key); }, transformReply: { 2: (reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping): TopKInfoReplyMap => { diff --git a/packages/bloom/lib/commands/top-k/LIST.spec.ts b/packages/bloom/lib/commands/top-k/LIST.spec.ts index 7ab96182bbe..8f5d0efa4db 100644 --- a/packages/bloom/lib/commands/top-k/LIST.spec.ts +++ b/packages/bloom/lib/commands/top-k/LIST.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import LIST from './LIST'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TOPK.LIST', () => { it('transformArguments', () => { assert.deepEqual( - LIST.transformArguments('key'), + parseArgs(LIST, 'key'), ['TOPK.LIST', 'key'] ); }); diff --git a/packages/bloom/lib/commands/top-k/LIST.ts b/packages/bloom/lib/commands/top-k/LIST.ts index 26345b72462..74b85e0f71d 100644 --- a/packages/bloom/lib/commands/top-k/LIST.ts +++ b/packages/bloom/lib/commands/top-k/LIST.ts @@ -1,10 +1,11 @@ -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['TOPK.LIST', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TOPK.LIST'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts index 862d17eb3e3..852170e8cd3 100644 --- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts +++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import LIST_WITHCOUNT from './LIST_WITHCOUNT'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TOPK.LIST WITHCOUNT', () => { testUtils.isVersionGreaterThanHook([2, 2, 9]); it('transformArguments', () => { assert.deepEqual( - LIST_WITHCOUNT.transformArguments('key'), + parseArgs(LIST_WITHCOUNT, 'key'), ['TOPK.LIST', 'key', 'WITHCOUNT'] ); }); diff --git a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts index d26936fd3c7..a3a3d3f43f8 100644 --- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts +++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts @@ -1,10 +1,12 @@ -import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['TOPK.LIST', key, 'WITHCOUNT']; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TOPK.LIST'); + parser.pushKey(key); + parser.push('WITHCOUNT'); }, transformReply(rawReply: UnwrapReply>) { const reply: Array<{ diff --git a/packages/bloom/lib/commands/top-k/QUERY.spec.ts b/packages/bloom/lib/commands/top-k/QUERY.spec.ts index d5ecfebb6c6..3651ec5d37b 100644 --- a/packages/bloom/lib/commands/top-k/QUERY.spec.ts +++ b/packages/bloom/lib/commands/top-k/QUERY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import QUERY from './QUERY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TOPK.QUERY', () => { it('transformArguments', () => { assert.deepEqual( - QUERY.transformArguments('key', 'item'), + parseArgs(QUERY, 'key', 'item'), ['TOPK.QUERY', 'key', 'item'] ); }); diff --git a/packages/bloom/lib/commands/top-k/QUERY.ts b/packages/bloom/lib/commands/top-k/QUERY.ts index 5529d4ab838..49b91714374 100644 --- a/packages/bloom/lib/commands/top-k/QUERY.ts +++ b/packages/bloom/lib/commands/top-k/QUERY.ts @@ -1,11 +1,13 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments, transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, items: RedisVariadicArgument) { - return pushVariadicArguments(['TOPK.QUERY', key], items); + parseCommand(parser: CommandParser, key: RedisArgument, items: RedisVariadicArgument) { + parser.push('TOPK.QUERY'); + parser.pushKey(key); + parser.pushVariadic(items); }, transformReply: transformBooleanArrayReply } as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/RESERVE.spec.ts b/packages/bloom/lib/commands/top-k/RESERVE.spec.ts index 39d8fb7efc6..aa8d194f940 100644 --- a/packages/bloom/lib/commands/top-k/RESERVE.spec.ts +++ b/packages/bloom/lib/commands/top-k/RESERVE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../../test-utils'; import RESERVE from './RESERVE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TOPK.RESERVE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - RESERVE.transformArguments('topK', 3), + parseArgs(RESERVE, 'topK', 3), ['TOPK.RESERVE', 'topK', '3'] ); }); it('with options', () => { assert.deepEqual( - RESERVE.transformArguments('topK', 3, { + parseArgs(RESERVE, 'topK', 3, { width: 8, depth: 7, decay: 0.9 diff --git a/packages/bloom/lib/commands/top-k/RESERVE.ts b/packages/bloom/lib/commands/top-k/RESERVE.ts index 12671728fea..f3ef3bf7200 100644 --- a/packages/bloom/lib/commands/top-k/RESERVE.ts +++ b/packages/bloom/lib/commands/top-k/RESERVE.ts @@ -1,4 +1,5 @@ -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; export interface TopKReserveOptions { width: number; @@ -7,20 +8,19 @@ export interface TopKReserveOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, topK: number, options?: TopKReserveOptions) { - const args = ['TOPK.RESERVE', key, topK.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, topK: number, options?: TopKReserveOptions) { + parser.push('TOPK.RESERVE'); + parser.pushKey(key); + parser.push(topK.toString()); if (options) { - args.push( + parser.push( options.width.toString(), options.depth.toString(), options.decay.toString() ); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/bloom/lib/commands/top-k/index.ts b/packages/bloom/lib/commands/top-k/index.ts index fb5de543cab..0352745fd07 100644 --- a/packages/bloom/lib/commands/top-k/index.ts +++ b/packages/bloom/lib/commands/top-k/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import type { RedisCommands } from '@redis/client/lib/RESP/types'; import ADD from './ADD'; import COUNT from './COUNT'; import INCRBY from './INCRBY'; diff --git a/packages/client/lib/RESP/encoder.ts b/packages/client/lib/RESP/encoder.ts index af857711dc3..995650627f1 100644 --- a/packages/client/lib/RESP/encoder.ts +++ b/packages/client/lib/RESP/encoder.ts @@ -2,7 +2,7 @@ import { RedisArgument } from './types'; const CRLF = '\r\n'; -export default function encodeCommand(args: Array): Array { +export default function encodeCommand(args: ReadonlyArray): ReadonlyArray { const toWrite: Array = []; let strings = '*' + args.length + CRLF; diff --git a/packages/client/lib/RESP/types.ts b/packages/client/lib/RESP/types.ts index 46fcd7ac8c1..692c433a49d 100644 --- a/packages/client/lib/RESP/types.ts +++ b/packages/client/lib/RESP/types.ts @@ -1,3 +1,5 @@ +import { CommandParser } from '../client/parser'; +import { Tail } from '../commands/generic-transformers'; import { BlobError, SimpleError } from '../errors'; import { RedisScriptConfig, SHA1 } from '../lua-script'; import { RESP_TYPES } from './decoder'; @@ -272,15 +274,16 @@ export type CommandArguments = Array & { preserve?: unknown }; // }; export type Command = { - FIRST_KEY_INDEX?: number | ((this: void, ...args: Array) => RedisArgument | undefined); + CACHEABLE?: boolean; IS_READ_ONLY?: boolean; /** * @internal * TODO: remove once `POLICIES` is implemented */ IS_FORWARD_COMMAND?: boolean; + NOT_KEYED_COMMAND?: true; // POLICIES?: CommandPolicies; - transformArguments(this: void, ...args: Array): CommandArguments; + parseCommand(this: void, parser: CommandParser, ...args: Array): void; TRANSFORM_LEGACY_REPLY?: boolean; transformReply: TransformReply | Record; unstableResp3?: boolean; @@ -365,7 +368,7 @@ export type CommandSignature< COMMAND extends Command, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping -> = (...args: Parameters) => Promise, TYPE_MAPPING>>; +> = (...args: Tail>) => Promise, TYPE_MAPPING>>; // export type CommandWithPoliciesSignature< // COMMAND extends Command, diff --git a/packages/client/lib/client/commands-queue.ts b/packages/client/lib/client/commands-queue.ts index a4029779fc8..15e8a747b98 100644 --- a/packages/client/lib/client/commands-queue.ts +++ b/packages/client/lib/client/commands-queue.ts @@ -1,7 +1,7 @@ import { SinglyLinkedList, DoublyLinkedNode, DoublyLinkedList } from './linked-list'; import encodeCommand from '../RESP/encoder'; import { Decoder, PUSH_TYPE_MAPPING, RESP_TYPES } from '../RESP/decoder'; -import { CommandArguments, TypeMapping, ReplyUnion, RespVersions } from '../RESP/types'; +import { TypeMapping, ReplyUnion, RespVersions, RedisArgument } from '../RESP/types'; import { ChannelListeners, PubSub, PubSubCommand, PubSubListener, PubSubType, PubSubTypeListeners } from './pub-sub'; import { AbortError, ErrorReply } from '../errors'; import { MonitorCallback } from '.'; @@ -17,7 +17,7 @@ export interface CommandOptions { } export interface CommandToWrite extends CommandWaitingForReply { - args: CommandArguments; + args: ReadonlyArray; chainId: symbol | undefined; abort: { signal: AbortSignal; @@ -117,7 +117,7 @@ export default class RedisCommandsQueue { } addCommand( - args: CommandArguments, + args: ReadonlyArray, options?: CommandOptions ): Promise { if (this.#maxLength && this.#toWrite.length + this.#waitingForReply.length >= this.#maxLength) { @@ -346,7 +346,7 @@ export default class RedisCommandsQueue { *commandsToWrite() { let toSend = this.#toWrite.shift(); while (toSend) { - let encoded: CommandArguments; + let encoded: ReadonlyArray try { encoded = encodeCommand(toSend.args); } catch (err) { diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index 50ed3d39da1..cd2040ec97f 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -9,6 +9,7 @@ import { MATH_FUNCTION, loadMathFunction } from '../commands/FUNCTION_LOAD.spec' import { RESP_TYPES } from '../RESP/decoder'; import { BlobStringReply, NumberReply } from '../RESP/types'; import { SortedSetMember } from '../commands/generic-transformers'; +import { CommandParser } from './parser'; export const SQUARE_SCRIPT = defineScript({ SCRIPT: @@ -16,8 +17,8 @@ export const SQUARE_SCRIPT = defineScript({ return number * number`, NUMBER_OF_KEYS: 1, FIRST_KEY_INDEX: 0, - transformArguments(key: string) { - return [key]; + parseCommand(parser: CommandParser, key: string) { + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply }); @@ -318,8 +319,8 @@ describe('Client', () => { const module = { echo: { - transformArguments(message: string) { - return ['ECHO', message]; + parseCommand(parser: CommandParser, message: string) { + parser.push('ECHO', message); }, transformReply: undefined as unknown as () => BlobStringReply } diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 64a3b578815..55355a133dd 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -7,14 +7,15 @@ import { ClientClosedError, ClientOfflineError, DisconnectsClientError, WatchErr import { URL } from 'node:url'; import { TcpSocketConnectOpts } from 'node:net'; import { PUBSUB_TYPE, PubSubType, PubSubListener, PubSubTypeListeners, ChannelListeners } from './pub-sub'; -import { Command, CommandSignature, TypeMapping, CommanderConfig, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, RedisArgument, ReplyWithTypeMapping, SimpleStringReply } from '../RESP/types'; +import { Command, CommandSignature, TypeMapping, CommanderConfig, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, RedisArgument, ReplyWithTypeMapping, SimpleStringReply, TransformReply } from '../RESP/types'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; import { RedisMultiQueuedCommand } from '../multi-command'; import HELLO, { HelloOptions } from '../commands/HELLO'; import { ScanOptions, ScanCommonOptions } from '../commands/SCAN'; import { RedisLegacyClient, RedisLegacyClientType } from './legacy-mode'; import { RedisPoolOptions, RedisClientPool } from './pool'; -import { RedisVariadicArgument, pushVariadicArguments } from '../commands/generic-transformers'; +import { RedisVariadicArgument, parseArgs, pushVariadicArguments } from '../commands/generic-transformers'; +import { BasicCommandParser, CommandParser } from './parser'; export interface RedisClientOptions< M extends RedisModules = RedisModules, @@ -151,64 +152,50 @@ export default class RedisClient< > extends EventEmitter { static #createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); - return async function (this: ProxyClient, ...args: Array) { - const redisArgs = command.transformArguments(...args); - const typeMapping = this._commandOptions?.typeMapping; - const reply = await this.sendCommand(redisArgs, this._commandOptions); + return async function (this: ProxyClient, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - return transformReply ? - transformReply(reply, redisArgs.preserve, typeMapping) : - reply; - }; + return this._self._executeCommand(command, parser, this._commandOptions, transformReply); + } } static #createModuleCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); - return async function (this: NamespaceProxyClient, ...args: Array) { - const redisArgs = command.transformArguments(...args); - const typeMapping = this._self._commandOptions?.typeMapping - const reply = await this._self.sendCommand(redisArgs, this._self._commandOptions); + return async function (this: NamespaceProxyClient, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - return transformReply ? - transformReply(reply, redisArgs.preserve, typeMapping) : - reply; + return this._self._executeCommand(command, parser, this._self._commandOptions, transformReply); }; } static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { - const prefix = functionArgumentsPrefix(name, fn), - transformReply = getTransformReply(fn, resp); - return async function (this: NamespaceProxyClient, ...args: Array) { - const fnArgs = fn.transformArguments(...args); - const typeMapping = this._self._commandOptions?.typeMapping; + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); - const reply = await this._self.sendCommand( - prefix.concat(fnArgs), - this._self._commandOptions - ); + return async function (this: NamespaceProxyClient, ...args: Array) { + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); - return transformReply ? - transformReply(reply, fnArgs.preserve, typeMapping) : - reply; + return this._self._executeCommand(fn, parser, this._self._commandOptions, transformReply); }; } static #createScriptCommand(script: RedisScript, resp: RespVersions) { - const prefix = scriptArgumentsPrefix(script), - transformReply = getTransformReply(script, resp); + const prefix = scriptArgumentsPrefix(script); + const transformReply = getTransformReply(script, resp); + return async function (this: ProxyClient, ...args: Array) { - const scriptArgs = script.transformArguments(...args); - const redisArgs = prefix.concat(scriptArgs); - const typeMapping = this._commandOptions?.typeMapping; - - const reply = await this.executeScript(script, redisArgs, this._commandOptions); - - return transformReply ? - transformReply(reply, scriptArgs.preserve, typeMapping) : - reply; - }; + const parser = new BasicCommandParser(); + parser.push(...prefix); + script.parseCommand(parser, ...args) + + return this._executeScript(script, parser, this._commandOptions, transformReply); + } } static factory< @@ -376,12 +363,12 @@ export default class RedisClient< } commands.push( - HELLO.transformArguments(this.#options.RESP, hello) + parseArgs(HELLO, this.#options.RESP, hello) ); } else { if (this.#options?.username || this.#options?.password) { commands.push( - COMMANDS.AUTH.transformArguments({ + parseArgs(COMMANDS.AUTH, { username: this.#options.username, password: this.#options.password ?? '' }) @@ -390,7 +377,7 @@ export default class RedisClient< if (this.#options?.name) { commands.push( - COMMANDS.CLIENT_SETNAME.transformArguments(this.#options.name) + parseArgs(COMMANDS.CLIENT_SETNAME, this.#options.name) ); } } @@ -401,7 +388,7 @@ export default class RedisClient< if (this.#options?.readonly) { commands.push( - COMMANDS.READONLY.transformArguments() + parseArgs(COMMANDS.READONLY) ); } @@ -585,35 +572,64 @@ export default class RedisClient< return this as unknown as RedisClientType; } - sendCommand( - args: Array, - options?: CommandOptions - ): Promise { - if (!this._self.#socket.isOpen) { - return Promise.reject(new ClientClosedError()); - } else if (!this._self.#socket.isReady && this._self.#options?.disableOfflineQueue) { - return Promise.reject(new ClientOfflineError()); + /** + * @internal + */ + async _executeCommand( + command: Command, + parser: CommandParser, + commandOptions: CommandOptions | undefined, + transformReply: TransformReply | undefined, + ) { + const reply = await this.sendCommand(parser.redisArgs, commandOptions); + + if (transformReply) { + return transformReply(reply, parser.preserve, commandOptions?.typeMapping); } - const promise = this._self.#queue.addCommand(args, options); - this._self.#scheduleWrite(); - return promise; + return reply; } - async executeScript( + /** + * @internal + */ + async _executeScript( script: RedisScript, - args: Array, - options?: CommandOptions + parser: CommandParser, + options: CommandOptions | undefined, + transformReply: TransformReply | undefined, ) { + const args = parser.redisArgs as Array; + + let reply: ReplyUnion; try { - return await this.sendCommand(args, options); + reply = await this.sendCommand(args, options); } catch (err) { if (!(err as Error)?.message?.startsWith?.('NOSCRIPT')) throw err; args[0] = 'EVAL'; args[1] = script.SCRIPT; - return await this.sendCommand(args, options); + reply = await this.sendCommand(args, options); + } + + return transformReply ? + transformReply(reply, parser.preserve, options?.typeMapping) : + reply; + } + + sendCommand( + args: ReadonlyArray, + options?: CommandOptions + ): Promise { + if (!this._self.#socket.isOpen) { + return Promise.reject(new ClientClosedError()); + } else if (!this._self.#socket.isReady && this._self.#options?.disableOfflineQueue) { + return Promise.reject(new ClientOfflineError()); } + + const promise = this._self.#queue.addCommand(args, options); + this._self.#scheduleWrite(); + return promise; } async SELECT(db: number): Promise { diff --git a/packages/client/lib/client/multi-command.ts b/packages/client/lib/client/multi-command.ts index b6579fcf9bf..a687655b60a 100644 --- a/packages/client/lib/client/multi-command.ts +++ b/packages/client/lib/client/multi-command.ts @@ -2,6 +2,8 @@ import COMMANDS from '../commands'; import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType, RedisMultiQueuedCommand } from '../multi-command'; import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping } from '../RESP/types'; import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander'; +import { BasicCommandParser } from './parser'; +import { Tail } from '../commands/generic-transformers'; type CommandSignature< REPLIES extends Array, @@ -11,7 +13,7 @@ type CommandSignature< S extends RedisScripts, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping -> = (...args: Parameters) => RedisClientMultiCommandType< +> = (...args: Tail>) => RedisClientMultiCommandType< [...REPLIES, ReplyWithTypeMapping, TYPE_MAPPING>], M, F, @@ -88,9 +90,16 @@ type ExecuteMulti = (commands: Array, selectedDB?: numb export default class RedisClientMultiCommand { static #createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + return function (this: RedisClientMultiCommand, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + return this.addCommand( - command.transformArguments(...args), + redisArgs, transformReply ); }; @@ -98,21 +107,33 @@ export default class RedisClientMultiCommand { static #createModuleCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + return function (this: { _self: RedisClientMultiCommand }, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + return this._self.addCommand( - command.transformArguments(...args), + redisArgs, transformReply ); }; } static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { - const prefix = functionArgumentsPrefix(name, fn), - transformReply = getTransformReply(fn, resp); + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); + return function (this: { _self: RedisClientMultiCommand }, ...args: Array) { - const fnArgs = fn.transformArguments(...args), - redisArgs: CommandArguments = prefix.concat(fnArgs); - redisArgs.preserve = fnArgs.preserve; + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + return this._self.addCommand( redisArgs, transformReply @@ -122,13 +143,19 @@ export default class RedisClientMultiCommand { static #createScriptCommand(script: RedisScript, resp: RespVersions) { const transformReply = getTransformReply(script, resp); + return function (this: RedisClientMultiCommand, ...args: Array) { - this.#multi.addScript( + const parser = new BasicCommandParser(); + script.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + + return this.#addScript( script, - script.transformArguments(...args), + redisArgs, transformReply ); - return this; }; } @@ -149,17 +176,16 @@ export default class RedisClientMultiCommand { }); } - readonly #multi = new RedisMultiCommand(); + readonly #multi: RedisMultiCommand readonly #executeMulti: ExecuteMulti; readonly #executePipeline: ExecuteMulti; - readonly #typeMapping?: TypeMapping; #selectedDB?: number; constructor(executeMulti: ExecuteMulti, executePipeline: ExecuteMulti, typeMapping?: TypeMapping) { + this.#multi = new RedisMultiCommand(typeMapping); this.#executeMulti = executeMulti; this.#executePipeline = executePipeline; - this.#typeMapping = typeMapping; } SELECT(db: number, transformReply?: TransformReply): this { @@ -175,12 +201,21 @@ export default class RedisClientMultiCommand { return this; } + #addScript( + script: RedisScript, + args: CommandArguments, + transformReply?: TransformReply + ) { + this.#multi.addScript(script, args, transformReply); + + return this; + } + async exec(execAsPipeline = false): Promise> { if (execAsPipeline) return this.execAsPipeline(); return this.#multi.transformReplies( - await this.#executeMulti(this.#multi.queue, this.#selectedDB), - this.#typeMapping + await this.#executeMulti(this.#multi.queue, this.#selectedDB) ) as MultiReplyType; } @@ -194,8 +229,7 @@ export default class RedisClientMultiCommand { if (this.#multi.queue.length === 0) return [] as MultiReplyType; return this.#multi.transformReplies( - await this.#executePipeline(this.#multi.queue, this.#selectedDB), - this.#typeMapping + await this.#executePipeline(this.#multi.queue, this.#selectedDB) ) as MultiReplyType; } diff --git a/packages/client/lib/client/parser.ts b/packages/client/lib/client/parser.ts new file mode 100644 index 00000000000..12eec457739 --- /dev/null +++ b/packages/client/lib/client/parser.ts @@ -0,0 +1,92 @@ +import { RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument } from '../commands/generic-transformers'; + +export interface CommandParser { + redisArgs: ReadonlyArray; + keys: ReadonlyArray; + firstKey: RedisArgument | undefined; + preserve: unknown; + + push: (...arg: Array) => unknown; + pushVariadic: (vals: RedisVariadicArgument) => unknown; + pushVariadicWithLength: (vals: RedisVariadicArgument) => unknown; + pushVariadicNumber: (vals: number | Array) => unknown; + pushKey: (key: RedisArgument) => unknown; // normal push of keys + pushKeys: (keys: RedisVariadicArgument) => unknown; // push multiple keys at a time + pushKeysLength: (keys: RedisVariadicArgument) => unknown; // push multiple keys at a time +} + +export class BasicCommandParser implements CommandParser { + #redisArgs: Array = []; + #keys: Array = []; + preserve: unknown; + + get redisArgs() { + return this.#redisArgs; + } + + get keys() { + return this.#keys; + } + + get firstKey() { + return this.#keys[0]; + } + + push(...arg: Array) { + this.#redisArgs.push(...arg); + }; + + pushVariadic(vals: RedisVariadicArgument) { + if (Array.isArray(vals)) { + for (const val of vals) { + this.push(val); + } + } else { + this.push(vals); + } + } + + pushVariadicWithLength(vals: RedisVariadicArgument) { + if (Array.isArray(vals)) { + this.#redisArgs.push(vals.length.toString()); + } else { + this.#redisArgs.push('1'); + } + this.pushVariadic(vals); + } + + pushVariadicNumber(vals: number | number[]) { + if (Array.isArray(vals)) { + for (const val of vals) { + this.push(val.toString()); + } + } else { + this.push(vals.toString()); + } + } + + pushKey(key: RedisArgument) { + this.#keys.push(key); + this.#redisArgs.push(key); + } + + pushKeysLength(keys: RedisVariadicArgument) { + if (Array.isArray(keys)) { + this.#redisArgs.push(keys.length.toString()); + } else { + this.#redisArgs.push('1'); + } + this.pushKeys(keys); + } + + pushKeys(keys: RedisVariadicArgument) { + if (Array.isArray(keys)) { + this.#keys.push(...keys); + this.#redisArgs.push(...keys); + } else { + this.#keys.push(keys); + this.#redisArgs.push(keys); + } + } +} diff --git a/packages/client/lib/client/pool.ts b/packages/client/lib/client/pool.ts index 4bd99ece8b6..a08377e3d38 100644 --- a/packages/client/lib/client/pool.ts +++ b/packages/client/lib/client/pool.ts @@ -7,6 +7,7 @@ import { TimeoutError } from '../errors'; import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; import { CommandOptions } from './commands-queue'; import RedisClientMultiCommand, { RedisClientMultiCommandType } from './multi-command'; +import { BasicCommandParser } from './parser'; export interface RedisPoolOptions { /** @@ -64,63 +65,48 @@ export class RedisClientPool< > extends EventEmitter { static #createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); - return async function (this: ProxyPool, ...args: Array) { - const redisArgs = command.transformArguments(...args); - const typeMapping = this._commandOptions?.typeMapping; - const reply = await this.sendCommand(redisArgs, this._commandOptions); + return async function (this: ProxyPool, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - return transformReply ? - transformReply(reply, redisArgs.preserve, typeMapping) : - reply; + return this.execute(client => client._executeCommand(command, parser, this._commandOptions, transformReply)) }; } static #createModuleCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); - return async function (this: NamespaceProxyPool, ...args: Array) { - const redisArgs = command.transformArguments(...args); - const typeMapping = this._self._commandOptions?.typeMapping; - const reply = await this._self.sendCommand(redisArgs, this._self._commandOptions); + return async function (this: NamespaceProxyPool, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - return transformReply ? - transformReply(reply, redisArgs.preserve, typeMapping) : - reply; + return this._self.execute(client => client._executeCommand(command, parser, this._self._commandOptions, transformReply)) }; } static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { - const prefix = functionArgumentsPrefix(name, fn), - transformReply = getTransformReply(fn, resp); - return async function (this: NamespaceProxyPool, ...args: Array) { - const fnArgs = fn.transformArguments(...args); - const typeMapping = this._self._commandOptions?.typeMapping; + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); - const reply = await this._self.sendCommand( - prefix.concat(fnArgs), - this._self._commandOptions - ); + return async function (this: NamespaceProxyPool, ...args: Array) { + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); - return transformReply ? - transformReply(reply, fnArgs.preserve, typeMapping) : - reply; - }; + return this._self.execute(client => client._executeCommand(fn, parser, this._self._commandOptions, transformReply)) }; } static #createScriptCommand(script: RedisScript, resp: RespVersions) { - const prefix = scriptArgumentsPrefix(script), - transformReply = getTransformReply(script, resp); + const prefix = scriptArgumentsPrefix(script); + const transformReply = getTransformReply(script, resp); + return async function (this: ProxyPool, ...args: Array) { - const scriptArgs = script.transformArguments(...args); - const redisArgs = prefix.concat(scriptArgs); - const typeMapping = this._commandOptions?.typeMapping; + const parser = new BasicCommandParser(); + parser.pushVariadic(prefix); + script.parseCommand(parser, ...args); - const reply = await this.executeScript(script, redisArgs, this._commandOptions); - - return transformReply ? - transformReply(reply, scriptArgs.preserve, typeMapping) : - reply; + return this.execute(client => client._executeScript(script, parser, this._commandOptions, transformReply)) }; } @@ -426,14 +412,6 @@ export class RedisClientPool< return this.execute(client => client.sendCommand(args, options)); } - executeScript( - script: RedisScript, - args: Array, - options?: CommandOptions - ) { - return this.execute(client => client.executeScript(script, args, options)); - } - MULTI() { type Multi = new (...args: ConstructorParameters) => RedisClientMultiCommandType<[], M, F, S, RESP, TYPE_MAPPING>; return new ((this as any).Multi as Multi)( diff --git a/packages/client/lib/client/socket.ts b/packages/client/lib/client/socket.ts index 3c2666e1067..36afa36c04a 100644 --- a/packages/client/lib/client/socket.ts +++ b/packages/client/lib/client/socket.ts @@ -271,7 +271,7 @@ export default class RedisSocket extends EventEmitter { }); } - write(iterable: Iterable>) { + write(iterable: Iterable>) { if (!this.#socket) return; this.#socket.cork(); diff --git a/packages/client/lib/cluster/index.ts b/packages/client/lib/cluster/index.ts index 7d01b1a20fe..12928e71f12 100644 --- a/packages/client/lib/cluster/index.ts +++ b/packages/client/lib/cluster/index.ts @@ -9,6 +9,9 @@ import RedisClusterMultiCommand, { RedisClusterMultiCommandType } from './multi- import { PubSubListener } from '../client/pub-sub'; import { ErrorReply } from '../errors'; import { RedisTcpSocketOptions } from '../client/socket'; +import ASKING from '../commands/ASKING'; +import { BasicCommandParser } from '../client/parser'; +import { parseArgs } from '../commands/generic-transformers'; interface ClusterCommander< M extends RedisModules, @@ -69,7 +72,7 @@ export interface RedisClusterOptions< type ClusterCommand< NAME extends PropertyKey, COMMAND extends Command -> = COMMAND['FIRST_KEY_INDEX'] extends undefined ? ( +> = COMMAND['NOT_KEYED_COMMAND'] extends true ? ( COMMAND['IS_FORWARD_COMMAND'] extends true ? NAME : never ) : NAME; @@ -143,131 +146,70 @@ export default class RedisCluster< TYPE_MAPPING extends TypeMapping, // POLICIES extends CommandPolicies > extends EventEmitter { - static extractFirstKey( - command: C, - args: Parameters, - redisArgs: Array - ) { - let key: RedisArgument | undefined; - switch (typeof command.FIRST_KEY_INDEX) { - case 'number': - key = redisArgs[command.FIRST_KEY_INDEX]; - break; - - case 'function': - key = command.FIRST_KEY_INDEX(...args); - break; - } - - return key; - } - static #createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); return async function (this: ProxyCluster, ...args: Array) { - const redisArgs = command.transformArguments(...args); - const typeMapping = this._commandOptions?.typeMapping; - - const firstKey = RedisCluster.extractFirstKey( - command, - args, - redisArgs - ); + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - const reply = await this.sendCommand( - firstKey, + return this._self.#execute( + parser.firstKey, command.IS_READ_ONLY, - redisArgs, this._commandOptions, - // command.POLICIES + (client, opts) => client._executeCommand(command, parser, opts, transformReply) ); - - return transformReply ? - transformReply(reply, redisArgs.preserve, typeMapping) : - reply; }; } static #createModuleCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); - return async function (this: NamespaceProxyCluster, ...args: Array) { - const redisArgs = command.transformArguments(...args); - const typeMapping = this._self._commandOptions?.typeMapping; - const firstKey = RedisCluster.extractFirstKey( - command, - args, - redisArgs - ); + return async function (this: NamespaceProxyCluster, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - const reply = await this._self.sendCommand( - firstKey, + return this._self.#execute( + parser.firstKey, command.IS_READ_ONLY, - redisArgs, this._self._commandOptions, - // command.POLICIES + (client, opts) => client._executeCommand(command, parser, opts, transformReply) ); - - return transformReply ? - transformReply(reply, redisArgs.preserve, typeMapping) : - reply; }; } static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { - const prefix = functionArgumentsPrefix(name, fn), - transformReply = getTransformReply(fn, resp); + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); + return async function (this: NamespaceProxyCluster, ...args: Array) { - const fnArgs = fn.transformArguments(...args); - const redisArgs = prefix.concat(fnArgs); - const typeMapping = this._self._commandOptions?.typeMapping; - - const firstKey = RedisCluster.extractFirstKey( - fn, - args, - fnArgs - ); + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); - const reply = await this._self.sendCommand( - firstKey, + return this._self.#execute( + parser.firstKey, fn.IS_READ_ONLY, - redisArgs, this._self._commandOptions, - // fn.POLICIES + (client, opts) => client._executeCommand(fn, parser, opts, transformReply) ); - - return transformReply ? - transformReply(reply, fnArgs.preserve, typeMapping) : - reply; }; } static #createScriptCommand(script: RedisScript, resp: RespVersions) { - const prefix = scriptArgumentsPrefix(script), - transformReply = getTransformReply(script, resp); + const prefix = scriptArgumentsPrefix(script); + const transformReply = getTransformReply(script, resp); + return async function (this: ProxyCluster, ...args: Array) { - const scriptArgs = script.transformArguments(...args); - const redisArgs = prefix.concat(scriptArgs); - const typeMapping = this._commandOptions?.typeMapping; - - const firstKey = RedisCluster.extractFirstKey( - script, - args, - scriptArgs - ); + const parser = new BasicCommandParser(); + parser.push(...prefix); + script.parseCommand(parser, ...args); - const reply = await this.executeScript( - script, - firstKey, + return this._self.#execute( + parser.firstKey, script.IS_READ_ONLY, - redisArgs, this._commandOptions, - // script.POLICIES + (client, opts) => client._executeScript(script, parser, opts, transformReply) ); - - return transformReply ? - transformReply(reply, scriptArgs.preserve, typeMapping) : - reply; }; } @@ -443,15 +385,20 @@ export default class RedisCluster< async #execute( firstKey: RedisArgument | undefined, isReadonly: boolean | undefined, - fn: (client: RedisClientType) => Promise + options: ClusterCommandOptions | undefined, + fn: (client: RedisClientType, opts?: ClusterCommandOptions) => Promise ): Promise { const maxCommandRedirections = this.#options.maxCommandRedirections ?? 16; - let client = await this.#slots.getClient(firstKey, isReadonly), - i = 0; + let client = await this.#slots.getClient(firstKey, isReadonly); + let i = 0; + let myOpts = options; + while (true) { try { - return await fn(client); + return await fn(client, myOpts); } catch (err) { + // reset to passed in options, if changed by an ask request + myOpts = options; // TODO: error class if (++i > maxCommandRedirections || !(err instanceof Error)) { throw err; @@ -469,8 +416,14 @@ export default class RedisCluster< throw new Error(`Cannot find node ${address}`); } - await redirectTo.asking(); client = redirectTo; + + const chainId = Symbol('Asking Chain'); + myOpts = options ? {...options} : {}; + myOpts.chainId = chainId; + + client.sendCommand(parseArgs(ASKING), {chainId: chainId}).catch(err => { console.log(`Asking Failed: ${err}`) } ); + continue; } @@ -495,21 +448,8 @@ export default class RedisCluster< return this._self.#execute( firstKey, isReadonly, - client => client.sendCommand(args, options) - ); - } - - executeScript( - script: RedisScript, - firstKey: RedisArgument | undefined, - isReadonly: boolean | undefined, - args: Array, - options?: CommandOptions - ) { - return this._self.#execute( - firstKey, - isReadonly, - client => client.executeScript(script, args, options) + options, + (client, opts) => client.sendCommand(args, opts) ); } diff --git a/packages/client/lib/cluster/multi-command.ts b/packages/client/lib/cluster/multi-command.ts index 2b02e8d7df2..f370618ff30 100644 --- a/packages/client/lib/cluster/multi-command.ts +++ b/packages/client/lib/cluster/multi-command.ts @@ -2,7 +2,8 @@ import COMMANDS from '../commands'; import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType, RedisMultiQueuedCommand } from '../multi-command'; import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping, RedisArgument } from '../RESP/types'; import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander'; -import RedisCluster from '.'; +import { BasicCommandParser } from '../client/parser'; +import { Tail } from '../commands/generic-transformers'; type CommandSignature< REPLIES extends Array, @@ -12,7 +13,7 @@ type CommandSignature< S extends RedisScripts, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping -> = (...args: Parameters) => RedisClusterMultiCommandType< +> = (...args: Tail>) => RedisClusterMultiCommandType< [...REPLIES, ReplyWithTypeMapping, TYPE_MAPPING>], M, F, @@ -93,13 +94,15 @@ export type ClusterMultiExecute = ( export default class RedisClusterMultiCommand { static #createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + return function (this: RedisClusterMultiCommand, ...args: Array) { - const redisArgs = command.transformArguments(...args); - const firstKey = RedisCluster.extractFirstKey( - command, - args, - redisArgs - ); + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + const firstKey = parser.firstKey; + return this.addCommand( firstKey, command.IS_READ_ONLY, @@ -111,13 +114,15 @@ export default class RedisClusterMultiCommand { static #createModuleCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + return function (this: { _self: RedisClusterMultiCommand }, ...args: Array) { - const redisArgs = command.transformArguments(...args), - firstKey = RedisCluster.extractFirstKey( - command, - args, - redisArgs - ); + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + const firstKey = parser.firstKey; + return this._self.addCommand( firstKey, command.IS_READ_ONLY, @@ -128,17 +133,18 @@ export default class RedisClusterMultiCommand { } static #createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { - const prefix = functionArgumentsPrefix(name, fn), - transformReply = getTransformReply(fn, resp); + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); + return function (this: { _self: RedisClusterMultiCommand }, ...args: Array) { - const fnArgs = fn.transformArguments(...args); - const redisArgs: CommandArguments = prefix.concat(fnArgs); - const firstKey = RedisCluster.extractFirstKey( - fn, - args, - fnArgs - ); - redisArgs.preserve = fnArgs.preserve; + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + const firstKey = parser.firstKey; + return this._self.addCommand( firstKey, fn.IS_READ_ONLY, @@ -150,22 +156,22 @@ export default class RedisClusterMultiCommand { static #createScriptCommand(script: RedisScript, resp: RespVersions) { const transformReply = getTransformReply(script, resp); + return function (this: RedisClusterMultiCommand, ...args: Array) { - const scriptArgs = script.transformArguments(...args); - this.#setState( - RedisCluster.extractFirstKey( - script, - args, - scriptArgs - ), - script.IS_READ_ONLY - ); - this.#multi.addScript( + const parser = new BasicCommandParser(); + script.parseCommand(parser, ...args); + + const scriptArgs: CommandArguments = parser.redisArgs; + scriptArgs.preserve = parser.preserve; + const firstKey = parser.firstKey; + + return this.#addScript( + firstKey, + script.IS_READ_ONLY, script, scriptArgs, transformReply ); - return this; }; } @@ -186,12 +192,12 @@ export default class RedisClusterMultiCommand { }); } - readonly #multi = new RedisMultiCommand(); + readonly #multi: RedisMultiCommand + readonly #executeMulti: ClusterMultiExecute; readonly #executePipeline: ClusterMultiExecute; #firstKey: RedisArgument | undefined; #isReadonly: boolean | undefined = true; - readonly #typeMapping?: TypeMapping; constructor( executeMulti: ClusterMultiExecute, @@ -199,10 +205,10 @@ export default class RedisClusterMultiCommand { routing: RedisArgument | undefined, typeMapping?: TypeMapping ) { + this.#multi = new RedisMultiCommand(typeMapping); this.#executeMulti = executeMulti; this.#executePipeline = executePipeline; this.#firstKey = routing; - this.#typeMapping = typeMapping; } #setState( @@ -224,6 +230,19 @@ export default class RedisClusterMultiCommand { return this; } + #addScript( + firstKey: RedisArgument | undefined, + isReadonly: boolean | undefined, + script: RedisScript, + args: CommandArguments, + transformReply?: TransformReply + ) { + this.#setState(firstKey, isReadonly); + this.#multi.addScript(script, args, transformReply); + + return this; + } + async exec(execAsPipeline = false) { if (execAsPipeline) return this.execAsPipeline(); @@ -232,8 +251,7 @@ export default class RedisClusterMultiCommand { this.#firstKey, this.#isReadonly, this.#multi.queue - ), - this.#typeMapping + ) ) as MultiReplyType; } @@ -251,8 +269,7 @@ export default class RedisClusterMultiCommand { this.#firstKey, this.#isReadonly, this.#multi.queue - ), - this.#typeMapping + ) ) as MultiReplyType; } diff --git a/packages/client/lib/commander.ts b/packages/client/lib/commander.ts index 4434317d267..6e5a2687cb1 100644 --- a/packages/client/lib/commander.ts +++ b/packages/client/lib/commander.ts @@ -1,4 +1,4 @@ -import { Command, CommanderConfig, RedisCommands, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions } from './RESP/types'; +import { Command, CommanderConfig, RedisArgument, RedisCommands, RedisFunction, RedisFunctions, RedisModules, RedisScript, RedisScripts, RespVersions, TransformReply } from './RESP/types'; interface AttachConfigOptions< M extends RedisModules, @@ -87,7 +87,7 @@ function attachNamespace(prototype: any, name: PropertyKey, fns: any) { }); } -export function getTransformReply(command: Command, resp: RespVersions) { +export function getTransformReply(command: Command, resp: RespVersions): TransformReply | undefined { switch (typeof command.transformReply) { case 'function': return command.transformReply; @@ -98,7 +98,7 @@ export function getTransformReply(command: Command, resp: RespVersions) { } export function functionArgumentsPrefix(name: string, fn: RedisFunction) { - const prefix: Array = [ + const prefix: Array = [ fn.IS_READ_ONLY ? 'FCALL_RO' : 'FCALL', name ]; diff --git a/packages/client/lib/commands/ACL_CAT.spec.ts b/packages/client/lib/commands/ACL_CAT.spec.ts index 2ce9d7db922..09d5ecade5a 100644 --- a/packages/client/lib/commands/ACL_CAT.spec.ts +++ b/packages/client/lib/commands/ACL_CAT.spec.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; import ACL_CAT from './ACL_CAT'; describe('ACL CAT', () => { @@ -8,14 +9,14 @@ describe('ACL CAT', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ACL_CAT.transformArguments(), + parseArgs(ACL_CAT), ['ACL', 'CAT'] ); }); it('with categoryName', () => { assert.deepEqual( - ACL_CAT.transformArguments('dangerous'), + parseArgs(ACL_CAT, 'dangerous'), ['ACL', 'CAT', 'dangerous'] ); }); diff --git a/packages/client/lib/commands/ACL_CAT.ts b/packages/client/lib/commands/ACL_CAT.ts index dd4762239a7..ae094b732b8 100644 --- a/packages/client/lib/commands/ACL_CAT.ts +++ b/packages/client/lib/commands/ACL_CAT.ts @@ -1,16 +1,14 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(categoryName?: RedisArgument) { - const args: Array = ['ACL', 'CAT']; - + parseCommand(parser: CommandParser, categoryName?: RedisArgument) { + parser.push('ACL', 'CAT'); if (categoryName) { - args.push(categoryName); + parser.push(categoryName); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_DELUSER.spec.ts b/packages/client/lib/commands/ACL_DELUSER.spec.ts index d6acbb22230..45fa3af9fc7 100644 --- a/packages/client/lib/commands/ACL_DELUSER.spec.ts +++ b/packages/client/lib/commands/ACL_DELUSER.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ACL_DELUSER from './ACL_DELUSER'; +import { parseArgs } from './generic-transformers'; describe('ACL DELUSER', () => { testUtils.isVersionGreaterThanHook([6]); @@ -8,14 +9,14 @@ describe('ACL DELUSER', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - ACL_DELUSER.transformArguments('username'), + parseArgs(ACL_DELUSER, 'username'), ['ACL', 'DELUSER', 'username'] ); }); it('array', () => { assert.deepEqual( - ACL_DELUSER.transformArguments(['1', '2']), + parseArgs(ACL_DELUSER, ['1', '2']), ['ACL', 'DELUSER', '1', '2'] ); }); diff --git a/packages/client/lib/commands/ACL_DELUSER.ts b/packages/client/lib/commands/ACL_DELUSER.ts index c0f8e15d672..5aa66becf75 100644 --- a/packages/client/lib/commands/ACL_DELUSER.ts +++ b/packages/client/lib/commands/ACL_DELUSER.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(username: RedisVariadicArgument) { - return pushVariadicArguments(['ACL', 'DELUSER'], username); + parseCommand(parser: CommandParser, username: RedisVariadicArgument) { + parser.push('ACL', 'DELUSER'); + parser.pushVariadic(username); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_DRYRUN.spec.ts b/packages/client/lib/commands/ACL_DRYRUN.spec.ts index 519092e0114..38a4def8361 100644 --- a/packages/client/lib/commands/ACL_DRYRUN.spec.ts +++ b/packages/client/lib/commands/ACL_DRYRUN.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ACL_DRYRUN from './ACL_DRYRUN'; +import { parseArgs } from './generic-transformers'; describe('ACL DRYRUN', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - ACL_DRYRUN.transformArguments('default', ['GET', 'key']), + parseArgs(ACL_DRYRUN, 'default', ['GET', 'key']), ['ACL', 'DRYRUN', 'default', 'GET', 'key'] ); }); diff --git a/packages/client/lib/commands/ACL_DRYRUN.ts b/packages/client/lib/commands/ACL_DRYRUN.ts index 257f0fe61e2..09a51bc36f8 100644 --- a/packages/client/lib/commands/ACL_DRYRUN.ts +++ b/packages/client/lib/commands/ACL_DRYRUN.ts @@ -1,15 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(username: RedisArgument, command: Array) { - return [ - 'ACL', - 'DRYRUN', - username, - ...command - ]; + parseCommand(parser: CommandParser, username: RedisArgument, command: Array) { + parser.push('ACL', 'DRYRUN', username, ...command); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_GENPASS.spec.ts b/packages/client/lib/commands/ACL_GENPASS.spec.ts index 44c1e167eb7..35e161f424f 100644 --- a/packages/client/lib/commands/ACL_GENPASS.spec.ts +++ b/packages/client/lib/commands/ACL_GENPASS.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ACL_GENPASS from './ACL_GENPASS'; +import { parseArgs } from './generic-transformers'; describe('ACL GENPASS', () => { testUtils.isVersionGreaterThanHook([6]); @@ -8,14 +9,14 @@ describe('ACL GENPASS', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ACL_GENPASS.transformArguments(), + parseArgs(ACL_GENPASS), ['ACL', 'GENPASS'] ); }); it('with bits', () => { assert.deepEqual( - ACL_GENPASS.transformArguments(128), + parseArgs(ACL_GENPASS, 128), ['ACL', 'GENPASS', '128'] ); }); diff --git a/packages/client/lib/commands/ACL_GENPASS.ts b/packages/client/lib/commands/ACL_GENPASS.ts index be89ff90a9a..b5caa29b9b2 100644 --- a/packages/client/lib/commands/ACL_GENPASS.ts +++ b/packages/client/lib/commands/ACL_GENPASS.ts @@ -1,16 +1,14 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(bits?: number) { - const args = ['ACL', 'GENPASS']; - + parseCommand(parser: CommandParser, bits?: number) { + parser.push('ACL', 'GENPASS'); if (bits) { - args.push(bits.toString()); + parser.push(bits.toString()); } - - return args; }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_GETUSER.spec.ts b/packages/client/lib/commands/ACL_GETUSER.spec.ts index 47351571127..83776a3473a 100644 --- a/packages/client/lib/commands/ACL_GETUSER.spec.ts +++ b/packages/client/lib/commands/ACL_GETUSER.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ACL_GETUSER from './ACL_GETUSER'; +import { parseArgs } from './generic-transformers'; describe('ACL GETUSER', () => { testUtils.isVersionGreaterThanHook([6]); it('transformArguments', () => { assert.deepEqual( - ACL_GETUSER.transformArguments('username'), + parseArgs(ACL_GETUSER, 'username'), ['ACL', 'GETUSER', 'username'] ); }); diff --git a/packages/client/lib/commands/ACL_GETUSER.ts b/packages/client/lib/commands/ACL_GETUSER.ts index cbbf48a4c69..b4764ad744e 100644 --- a/packages/client/lib/commands/ACL_GETUSER.ts +++ b/packages/client/lib/commands/ACL_GETUSER.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; type AclUser = TuplesToMapReply<[ @@ -17,10 +18,10 @@ type AclUser = TuplesToMapReply<[ ]>; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(username: RedisArgument) { - return ['ACL', 'GETUSER', username]; + parseCommand(parser: CommandParser, username: RedisArgument) { + parser.push('ACL', 'GETUSER', username); }, transformReply: { 2: (reply: UnwrapReply>) => ({ diff --git a/packages/client/lib/commands/ACL_LIST.spec.ts b/packages/client/lib/commands/ACL_LIST.spec.ts index b188cae30b0..0f67aaa53e9 100644 --- a/packages/client/lib/commands/ACL_LIST.spec.ts +++ b/packages/client/lib/commands/ACL_LIST.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ACL_LIST from './ACL_LIST'; +import { parseArgs } from './generic-transformers'; describe('ACL LIST', () => { testUtils.isVersionGreaterThanHook([6]); it('transformArguments', () => { assert.deepEqual( - ACL_LIST.transformArguments(), + parseArgs(ACL_LIST), ['ACL', 'LIST'] ); }); diff --git a/packages/client/lib/commands/ACL_LIST.ts b/packages/client/lib/commands/ACL_LIST.ts index 1a831a4987c..b5f82cf272c 100644 --- a/packages/client/lib/commands/ACL_LIST.ts +++ b/packages/client/lib/commands/ACL_LIST.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['ACL', 'LIST']; + parseCommand(parser: CommandParser) { + parser.push('ACL', 'LIST'); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_LOAD.spec.ts b/packages/client/lib/commands/ACL_LOAD.spec.ts index 68552164ce0..a41ce45e8a6 100644 --- a/packages/client/lib/commands/ACL_LOAD.spec.ts +++ b/packages/client/lib/commands/ACL_LOAD.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; import ACL_LOAD from './ACL_LOAD'; +import { parseArgs } from './generic-transformers'; describe('ACL LOAD', () => { testUtils.isVersionGreaterThanHook([6]); it('transformArguments', () => { assert.deepEqual( - ACL_LOAD.transformArguments(), + parseArgs(ACL_LOAD), ['ACL', 'LOAD'] ); }); diff --git a/packages/client/lib/commands/ACL_LOAD.ts b/packages/client/lib/commands/ACL_LOAD.ts index 39587829b17..dc4320b99fc 100644 --- a/packages/client/lib/commands/ACL_LOAD.ts +++ b/packages/client/lib/commands/ACL_LOAD.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['ACL', 'LOAD']; + parseCommand(parser: CommandParser) { + parser.push('ACL', 'LOAD'); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_LOG.spec.ts b/packages/client/lib/commands/ACL_LOG.spec.ts index b85a7076f4a..7da61faca37 100644 --- a/packages/client/lib/commands/ACL_LOG.spec.ts +++ b/packages/client/lib/commands/ACL_LOG.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ACL_LOG from './ACL_LOG'; +import { parseArgs } from './generic-transformers'; describe('ACL LOG', () => { testUtils.isVersionGreaterThanHook([6]); @@ -8,14 +9,14 @@ describe('ACL LOG', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ACL_LOG.transformArguments(), + parseArgs(ACL_LOG), ['ACL', 'LOG'] ); }); it('with count', () => { assert.deepEqual( - ACL_LOG.transformArguments(10), + parseArgs(ACL_LOG, 10), ['ACL', 'LOG', '10'] ); }); diff --git a/packages/client/lib/commands/ACL_LOG.ts b/packages/client/lib/commands/ACL_LOG.ts index 0f0a976e093..4cf2722ec86 100644 --- a/packages/client/lib/commands/ACL_LOG.ts +++ b/packages/client/lib/commands/ACL_LOG.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, TypeMapping } from '../RESP/types'; import { transformDoubleReply } from './generic-transformers'; @@ -18,16 +19,13 @@ export type AclLogReply = ArrayReply>; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(count?: number) { - const args = ['ACL', 'LOG']; - - if (count !== undefined) { - args.push(count.toString()); + parseCommand(parser: CommandParser, count?: number) { + parser.push('ACL', 'LOG'); + if (count != undefined) { + parser.push(count.toString()); } - - return args; }, transformReply: { 2: (reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { diff --git a/packages/client/lib/commands/ACL_LOG_RESET.spec.ts b/packages/client/lib/commands/ACL_LOG_RESET.spec.ts index 8849440c1a6..62d193a132d 100644 --- a/packages/client/lib/commands/ACL_LOG_RESET.spec.ts +++ b/packages/client/lib/commands/ACL_LOG_RESET.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ACL_LOG_RESET from './ACL_LOG_RESET'; +import { parseArgs } from './generic-transformers'; describe('ACL LOG RESET', () => { testUtils.isVersionGreaterThanHook([6]); it('transformArguments', () => { assert.deepEqual( - ACL_LOG_RESET.transformArguments(), + parseArgs(ACL_LOG_RESET), ['ACL', 'LOG', 'RESET'] ); }); diff --git a/packages/client/lib/commands/ACL_LOG_RESET.ts b/packages/client/lib/commands/ACL_LOG_RESET.ts index 91d58d538e9..9a692129bd2 100644 --- a/packages/client/lib/commands/ACL_LOG_RESET.ts +++ b/packages/client/lib/commands/ACL_LOG_RESET.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; import ACL_LOG from './ACL_LOG'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: ACL_LOG.IS_READ_ONLY, - transformArguments() { - return ['ACL', 'LOG', 'RESET']; + parseCommand(parser: CommandParser) { + parser.push('ACL', 'LOG', 'RESET'); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_SAVE.spec.ts b/packages/client/lib/commands/ACL_SAVE.spec.ts index 1fe402867e7..98f7c9f183d 100644 --- a/packages/client/lib/commands/ACL_SAVE.spec.ts +++ b/packages/client/lib/commands/ACL_SAVE.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; import ACL_SAVE from './ACL_SAVE'; +import { parseArgs } from './generic-transformers'; describe('ACL SAVE', () => { testUtils.isVersionGreaterThanHook([6]); it('transformArguments', () => { assert.deepEqual( - ACL_SAVE.transformArguments(), + parseArgs(ACL_SAVE), ['ACL', 'SAVE'] ); }); diff --git a/packages/client/lib/commands/ACL_SAVE.ts b/packages/client/lib/commands/ACL_SAVE.ts index 8c2e2dab115..ec24522724a 100644 --- a/packages/client/lib/commands/ACL_SAVE.ts +++ b/packages/client/lib/commands/ACL_SAVE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['ACL', 'SAVE']; + parseCommand(parser: CommandParser) { + parser.push('ACL', 'SAVE'); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_SETUSER.spec.ts b/packages/client/lib/commands/ACL_SETUSER.spec.ts index 10aea62ed02..9f39868e809 100644 --- a/packages/client/lib/commands/ACL_SETUSER.spec.ts +++ b/packages/client/lib/commands/ACL_SETUSER.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; import ACL_SETUSER from './ACL_SETUSER'; +import { parseArgs } from './generic-transformers'; describe('ACL SETUSER', () => { testUtils.isVersionGreaterThanHook([6]); @@ -8,14 +9,14 @@ describe('ACL SETUSER', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - ACL_SETUSER.transformArguments('username', 'allkeys'), + parseArgs(ACL_SETUSER, 'username', 'allkeys'), ['ACL', 'SETUSER', 'username', 'allkeys'] ); }); it('array', () => { assert.deepEqual( - ACL_SETUSER.transformArguments('username', ['allkeys', 'allchannels']), + parseArgs(ACL_SETUSER, 'username', ['allkeys', 'allchannels']), ['ACL', 'SETUSER', 'username', 'allkeys', 'allchannels'] ); }); diff --git a/packages/client/lib/commands/ACL_SETUSER.ts b/packages/client/lib/commands/ACL_SETUSER.ts index c99fec3d9ba..cad013f4d15 100644 --- a/packages/client/lib/commands/ACL_SETUSER.ts +++ b/packages/client/lib/commands/ACL_SETUSER.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(username: RedisArgument, rule: RedisVariadicArgument) { - return pushVariadicArguments(['ACL', 'SETUSER', username], rule); + parseCommand(parser: CommandParser, username: RedisArgument, rule: RedisVariadicArgument) { + parser.push('ACL', 'SETUSER', username); + parser.pushVariadic(rule); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_USERS.spec.ts b/packages/client/lib/commands/ACL_USERS.spec.ts index 2d433d4ec1e..d897b61e4f3 100644 --- a/packages/client/lib/commands/ACL_USERS.spec.ts +++ b/packages/client/lib/commands/ACL_USERS.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; import ACL_USERS from './ACL_USERS'; +import { parseArgs } from './generic-transformers'; describe('ACL USERS', () => { testUtils.isVersionGreaterThanHook([6]); it('transformArguments', () => { assert.deepEqual( - ACL_USERS.transformArguments(), + parseArgs(ACL_USERS), ['ACL', 'USERS'] ); }); diff --git a/packages/client/lib/commands/ACL_USERS.ts b/packages/client/lib/commands/ACL_USERS.ts index ee8c619f25f..6ce4c6d84ef 100644 --- a/packages/client/lib/commands/ACL_USERS.ts +++ b/packages/client/lib/commands/ACL_USERS.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['ACL', 'USERS']; + parseCommand(parser: CommandParser) { + parser.push('ACL', 'USERS'); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ACL_WHOAMI.spec.ts b/packages/client/lib/commands/ACL_WHOAMI.spec.ts index 24a5cbd1d63..f939c657a7a 100644 --- a/packages/client/lib/commands/ACL_WHOAMI.spec.ts +++ b/packages/client/lib/commands/ACL_WHOAMI.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; import ACL_WHOAMI from './ACL_WHOAMI'; +import { parseArgs } from './generic-transformers'; describe('ACL WHOAMI', () => { testUtils.isVersionGreaterThanHook([6]); it('transformArguments', () => { assert.deepEqual( - ACL_WHOAMI.transformArguments(), + parseArgs(ACL_WHOAMI), ['ACL', 'WHOAMI'] ); }); diff --git a/packages/client/lib/commands/ACL_WHOAMI.ts b/packages/client/lib/commands/ACL_WHOAMI.ts index 81a1c84a3c6..eb21a75af5f 100644 --- a/packages/client/lib/commands/ACL_WHOAMI.ts +++ b/packages/client/lib/commands/ACL_WHOAMI.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['ACL', 'WHOAMI']; + parseCommand(parser: CommandParser) { + parser.push('ACL', 'WHOAMI'); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/APPEND.spec.ts b/packages/client/lib/commands/APPEND.spec.ts index ca18a00ac42..925c16917b9 100644 --- a/packages/client/lib/commands/APPEND.spec.ts +++ b/packages/client/lib/commands/APPEND.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import APPEND from './APPEND'; +import { parseArgs } from './generic-transformers'; describe('APPEND', () => { it('transformArguments', () => { assert.deepEqual( - APPEND.transformArguments('key', 'value'), + parseArgs(APPEND, 'key', 'value'), ['APPEND', 'key', 'value'] ); }); diff --git a/packages/client/lib/commands/APPEND.ts b/packages/client/lib/commands/APPEND.ts index 1bc01024998..18fc5c7b3aa 100644 --- a/packages/client/lib/commands/APPEND.ts +++ b/packages/client/lib/commands/APPEND.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, value: RedisArgument) { - return ['APPEND', key, value]; + parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) { + parser.push('APPEND', key, value); }, + transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ASKING.spec.ts b/packages/client/lib/commands/ASKING.spec.ts index bd83bec599f..7be4d25d449 100644 --- a/packages/client/lib/commands/ASKING.spec.ts +++ b/packages/client/lib/commands/ASKING.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import ASKING from './ASKING'; +import { parseArgs } from './generic-transformers'; describe('ASKING', () => { it('transformArguments', () => { assert.deepEqual( - ASKING.transformArguments(), + parseArgs(ASKING), ['ASKING'] ); }); diff --git a/packages/client/lib/commands/ASKING.ts b/packages/client/lib/commands/ASKING.ts index c6ada477ee4..92ce8f72390 100644 --- a/packages/client/lib/commands/ASKING.ts +++ b/packages/client/lib/commands/ASKING.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; +export const ASKING_CMD = 'ASKING'; + export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['ASKING']; + parseCommand(parser: CommandParser) { + parser.push(ASKING_CMD); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/AUTH.spec.ts b/packages/client/lib/commands/AUTH.spec.ts index 2da016ba873..762dd24f16a 100644 --- a/packages/client/lib/commands/AUTH.spec.ts +++ b/packages/client/lib/commands/AUTH.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import AUTH from './AUTH'; +import { parseArgs } from './generic-transformers'; describe('AUTH', () => { describe('transformArguments', () => { it('password only', () => { assert.deepEqual( - AUTH.transformArguments({ + parseArgs(AUTH, { password: 'password' }), ['AUTH', 'password'] @@ -14,7 +15,7 @@ describe('AUTH', () => { it('username & password', () => { assert.deepEqual( - AUTH.transformArguments({ + parseArgs(AUTH, { username: 'username', password: 'password' }), diff --git a/packages/client/lib/commands/AUTH.ts b/packages/client/lib/commands/AUTH.ts index 4c7a0b0c765..85b48b0026e 100644 --- a/packages/client/lib/commands/AUTH.ts +++ b/packages/client/lib/commands/AUTH.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export interface AuthOptions { @@ -6,18 +7,14 @@ export interface AuthOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments({ username, password }: AuthOptions) { - const args: Array = ['AUTH']; - + parseCommand(parser: CommandParser, { username, password }: AuthOptions) { + parser.push('AUTH'); if (username !== undefined) { - args.push(username); + parser.push(username); } - - args.push(password); - - return args; + parser.push(password); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/BGREWRITEAOF.spec.ts b/packages/client/lib/commands/BGREWRITEAOF.spec.ts index 5447fc70a79..f58ec9a5762 100644 --- a/packages/client/lib/commands/BGREWRITEAOF.spec.ts +++ b/packages/client/lib/commands/BGREWRITEAOF.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import BGREWRITEAOF from './BGREWRITEAOF'; +import { parseArgs } from './generic-transformers'; describe('BGREWRITEAOF', () => { it('transformArguments', () => { assert.deepEqual( - BGREWRITEAOF.transformArguments(), + parseArgs(BGREWRITEAOF), ['BGREWRITEAOF'] ); }); diff --git a/packages/client/lib/commands/BGREWRITEAOF.ts b/packages/client/lib/commands/BGREWRITEAOF.ts index 5f9a46e318f..c658f3e8529 100644 --- a/packages/client/lib/commands/BGREWRITEAOF.ts +++ b/packages/client/lib/commands/BGREWRITEAOF.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['BGREWRITEAOF']; + parseCommand(parser: CommandParser) { + parser.push('BGREWRITEAOF'); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BGSAVE.spec.ts b/packages/client/lib/commands/BGSAVE.spec.ts index 7944722dd56..dcf7b815119 100644 --- a/packages/client/lib/commands/BGSAVE.spec.ts +++ b/packages/client/lib/commands/BGSAVE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import BGSAVE from './BGSAVE'; +import { parseArgs } from './generic-transformers'; describe('BGSAVE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - BGSAVE.transformArguments(), + parseArgs(BGSAVE), ['BGSAVE'] ); }); it('with SCHEDULE', () => { assert.deepEqual( - BGSAVE.transformArguments({ + parseArgs(BGSAVE, { SCHEDULE: true }), ['BGSAVE', 'SCHEDULE'] diff --git a/packages/client/lib/commands/BGSAVE.ts b/packages/client/lib/commands/BGSAVE.ts index dc0f5056708..1fd6c6b5bdb 100644 --- a/packages/client/lib/commands/BGSAVE.ts +++ b/packages/client/lib/commands/BGSAVE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export interface BgSaveOptions { @@ -5,16 +6,13 @@ export interface BgSaveOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(options?: BgSaveOptions) { - const args = ['BGSAVE']; - + parseCommand(parser: CommandParser, options?: BgSaveOptions) { + parser.push('BGSAVE'); if (options?.SCHEDULE) { - args.push('SCHEDULE'); + parser.push('SCHEDULE'); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BITCOUNT.spec.ts b/packages/client/lib/commands/BITCOUNT.spec.ts index ceb6476a31c..e2990472948 100644 --- a/packages/client/lib/commands/BITCOUNT.spec.ts +++ b/packages/client/lib/commands/BITCOUNT.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import BITCOUNT from './BITCOUNT'; +import { parseArgs } from './generic-transformers'; describe('BITCOUNT', () => { - describe('transformArguments', () => { + describe('parseCommand', () => { it('simple', () => { assert.deepEqual( - BITCOUNT.transformArguments('key'), + parseArgs(BITCOUNT, 'key'), ['BITCOUNT', 'key'] ); }); @@ -14,7 +15,7 @@ describe('BITCOUNT', () => { describe('with range', () => { it('simple', () => { assert.deepEqual( - BITCOUNT.transformArguments('key', { + parseArgs(BITCOUNT, 'key', { start: 0, end: 1 }), @@ -24,7 +25,7 @@ describe('BITCOUNT', () => { it('with mode', () => { assert.deepEqual( - BITCOUNT.transformArguments('key', { + parseArgs(BITCOUNT, 'key', { start: 0, end: 1, mode: 'BIT' diff --git a/packages/client/lib/commands/BITCOUNT.ts b/packages/client/lib/commands/BITCOUNT.ts index 6ec6b89be8b..decfb754db5 100644 --- a/packages/client/lib/commands/BITCOUNT.ts +++ b/packages/client/lib/commands/BITCOUNT.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export interface BitCountRange { @@ -7,23 +8,19 @@ export interface BitCountRange { } export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, range?: BitCountRange) { - const args = ['BITCOUNT', key]; - + parseCommand(parser: CommandParser, key: RedisArgument, range?: BitCountRange) { + parser.push('BITCOUNT'); + parser.pushKey(key); if (range) { - args.push( - range.start.toString(), - range.end.toString() - ); + parser.push(range.start.toString()); + parser.push(range.end.toString()); if (range.mode) { - args.push(range.mode); + parser.push(range.mode); } } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BITFIELD.spec.ts b/packages/client/lib/commands/BITFIELD.spec.ts index 7f805755493..5fcc112466b 100644 --- a/packages/client/lib/commands/BITFIELD.spec.ts +++ b/packages/client/lib/commands/BITFIELD.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import BITFIELD from './BITFIELD'; +import { parseArgs } from './generic-transformers'; describe('BITFIELD', () => { it('transformArguments', () => { assert.deepEqual( - BITFIELD.transformArguments('key', [{ + parseArgs(BITFIELD, 'key', [{ operation: 'OVERFLOW', behavior: 'WRAP' }, { diff --git a/packages/client/lib/commands/BITFIELD.ts b/packages/client/lib/commands/BITFIELD.ts index 5d7d4bf7282..f095b4cf7a6 100644 --- a/packages/client/lib/commands/BITFIELD.ts +++ b/packages/client/lib/commands/BITFIELD.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '../RESP/types'; export type BitFieldEncoding = `${'i' | 'u'}${number}`; @@ -39,15 +40,15 @@ export type BitFieldRoOperations = Array< >; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, operations: BitFieldOperations) { - const args = ['BITFIELD', key]; + parseCommand(parser: CommandParser, key: RedisArgument, operations: BitFieldOperations) { + parser.push('BITFIELD'); + parser.pushKey(key); for (const options of operations) { switch (options.operation) { case 'GET': - args.push( + parser.push( 'GET', options.encoding, options.offset.toString() @@ -55,7 +56,7 @@ export default { break; case 'SET': - args.push( + parser.push( 'SET', options.encoding, options.offset.toString(), @@ -64,7 +65,7 @@ export default { break; case 'INCRBY': - args.push( + parser.push( 'INCRBY', options.encoding, options.offset.toString(), @@ -73,15 +74,13 @@ export default { break; case 'OVERFLOW': - args.push( + parser.push( 'OVERFLOW', options.behavior ); break; } } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BITFIELD_RO.spec.ts b/packages/client/lib/commands/BITFIELD_RO.spec.ts index 0793100193f..f2c1797412f 100644 --- a/packages/client/lib/commands/BITFIELD_RO.spec.ts +++ b/packages/client/lib/commands/BITFIELD_RO.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import BITFIELD_RO from './BITFIELD_RO'; +import { parseArgs } from './generic-transformers'; describe('BITFIELD_RO', () => { testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { + it('parseCommand', () => { assert.deepEqual( - BITFIELD_RO.transformArguments('key', [{ + parseArgs(BITFIELD_RO, 'key', [{ encoding: 'i8', offset: 0 }]), diff --git a/packages/client/lib/commands/BITFIELD_RO.ts b/packages/client/lib/commands/BITFIELD_RO.ts index 99e500abc04..66001718b80 100644 --- a/packages/client/lib/commands/BITFIELD_RO.ts +++ b/packages/client/lib/commands/BITFIELD_RO.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; import { BitFieldGetOperation } from './BITFIELD'; @@ -6,20 +7,17 @@ export type BitFieldRoOperations = Array< >; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, operations: BitFieldRoOperations) { - const args = ['BITFIELD_RO', key]; + parseCommand(parser: CommandParser, key: RedisArgument, operations: BitFieldRoOperations) { + parser.push('BITFIELD_RO'); + parser.pushKey(key); for (const operation of operations) { - args.push( - 'GET', - operation.encoding, - operation.offset.toString() - ); + parser.push('GET'); + parser.push(operation.encoding); + parser.push(operation.offset.toString()) } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BITOP.spec.ts b/packages/client/lib/commands/BITOP.spec.ts index 4df1782467f..25fe48fc13c 100644 --- a/packages/client/lib/commands/BITOP.spec.ts +++ b/packages/client/lib/commands/BITOP.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import BITOP from './BITOP'; +import { parseArgs } from './generic-transformers'; describe('BITOP', () => { describe('transformArguments', () => { it('single key', () => { assert.deepEqual( - BITOP.transformArguments('AND', 'destKey', 'key'), + parseArgs(BITOP, 'AND', 'destKey', 'key'), ['BITOP', 'AND', 'destKey', 'key'] ); }); it('multiple keys', () => { assert.deepEqual( - BITOP.transformArguments('AND', 'destKey', ['1', '2']), + parseArgs(BITOP, 'AND', 'destKey', ['1', '2']), ['BITOP', 'AND', 'destKey', '1', '2'] ); }); diff --git a/packages/client/lib/commands/BITOP.ts b/packages/client/lib/commands/BITOP.ts index 4c34845699e..bb770148114 100644 --- a/packages/client/lib/commands/BITOP.ts +++ b/packages/client/lib/commands/BITOP.ts @@ -1,17 +1,20 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export type BitOperations = 'AND' | 'OR' | 'XOR' | 'NOT'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, operation: BitOperations, destKey: RedisArgument, key: RedisVariadicArgument ) { - return pushVariadicArguments(['BITOP', operation, destKey], key); + parser.push('BITOP', operation); + parser.pushKey(destKey); + parser.pushKeys(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BITPOS.spec.ts b/packages/client/lib/commands/BITPOS.spec.ts index 61940560057..c699deab83c 100644 --- a/packages/client/lib/commands/BITPOS.spec.ts +++ b/packages/client/lib/commands/BITPOS.spec.ts @@ -1,33 +1,34 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import BITPOS from './BITPOS'; +import { parseArgs } from './generic-transformers'; describe('BITPOS', () => { - describe('transformArguments', () => { + describe('parseCommand', () => { it('simple', () => { assert.deepEqual( - BITPOS.transformArguments('key', 1), + parseArgs(BITPOS, 'key', 1), ['BITPOS', 'key', '1'] ); }); it('with start', () => { assert.deepEqual( - BITPOS.transformArguments('key', 1, 1), + parseArgs(BITPOS, 'key', 1, 1), ['BITPOS', 'key', '1', '1'] ); }); it('with start and end', () => { assert.deepEqual( - BITPOS.transformArguments('key', 1, 1, -1), + parseArgs(BITPOS, 'key', 1, 1, -1), ['BITPOS', 'key', '1', '1', '-1'] ); }); it('with start, end and mode', () => { assert.deepEqual( - BITPOS.transformArguments('key', 1, 1, -1, 'BIT'), + parseArgs(BITPOS, 'key', 1, 1, -1, 'BIT'), ['BITPOS', 'key', '1', '1', '-1', 'BIT'] ); }); diff --git a/packages/client/lib/commands/BITPOS.ts b/packages/client/lib/commands/BITPOS.ts index 5d6276dffc0..57e3a63b681 100644 --- a/packages/client/lib/commands/BITPOS.ts +++ b/packages/client/lib/commands/BITPOS.ts @@ -1,31 +1,32 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { BitValue } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( + parseCommand(parser: CommandParser, key: RedisArgument, bit: BitValue, start?: number, end?: number, mode?: 'BYTE' | 'BIT' ) { - const args = ['BITPOS', key, bit.toString()]; + parser.push('BITPOS'); + parser.pushKey(key); + parser.push(bit.toString()); if (start !== undefined) { - args.push(start.toString()); + parser.push(start.toString()); } if (end !== undefined) { - args.push(end.toString()); + parser.push(end.toString()); } if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BLMOVE.spec.ts b/packages/client/lib/commands/BLMOVE.spec.ts index 0eca8c61005..d4e9e024a8c 100644 --- a/packages/client/lib/commands/BLMOVE.spec.ts +++ b/packages/client/lib/commands/BLMOVE.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; import BLMOVE from './BLMOVE'; +import { parseArgs } from './generic-transformers'; describe('BLMOVE', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - BLMOVE.transformArguments('source', 'destination', 'LEFT', 'RIGHT', 0), + parseArgs(BLMOVE, 'source', 'destination', 'LEFT', 'RIGHT', 0), ['BLMOVE', 'source', 'destination', 'LEFT', 'RIGHT', '0'] ); }); diff --git a/packages/client/lib/commands/BLMOVE.ts b/packages/client/lib/commands/BLMOVE.ts index c7e4844375f..b0ada7cdb20 100644 --- a/packages/client/lib/commands/BLMOVE.ts +++ b/packages/client/lib/commands/BLMOVE.ts @@ -1,24 +1,20 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; import { ListSide } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, source: RedisArgument, destination: RedisArgument, sourceSide: ListSide, destinationSide: ListSide, timeout: number ) { - return [ - 'BLMOVE', - source, - destination, - sourceSide, - destinationSide, - timeout.toString() - ]; + parser.push('BLMOVE'); + parser.pushKeys([source, destination]); + parser.push(sourceSide, destinationSide, timeout.toString()) }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BLMPOP.spec.ts b/packages/client/lib/commands/BLMPOP.spec.ts index b40556b1e46..6cda524b50f 100644 --- a/packages/client/lib/commands/BLMPOP.spec.ts +++ b/packages/client/lib/commands/BLMPOP.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; import BLMPOP from './BLMPOP'; +import { parseArgs } from './generic-transformers'; describe('BLMPOP', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,14 +9,14 @@ describe('BLMPOP', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - BLMPOP.transformArguments(0, 'key', 'LEFT'), + parseArgs(BLMPOP, 0, 'key', 'LEFT'), ['BLMPOP', '0', '1', 'key', 'LEFT'] ); }); it('with COUNT', () => { assert.deepEqual( - BLMPOP.transformArguments(0, 'key', 'LEFT', { + parseArgs(BLMPOP, 0, 'key', 'LEFT', { COUNT: 1 }), ['BLMPOP', '0', '1', 'key', 'LEFT', 'COUNT', '1'] diff --git a/packages/client/lib/commands/BLMPOP.ts b/packages/client/lib/commands/BLMPOP.ts index 3122e908600..15d03f8d822 100644 --- a/packages/client/lib/commands/BLMPOP.ts +++ b/packages/client/lib/commands/BLMPOP.ts @@ -1,17 +1,12 @@ +import { CommandParser } from '../client/parser'; import { Command } from '../RESP/types'; -import LMPOP, { LMPopArguments, transformLMPopArguments } from './LMPOP'; +import LMPOP, { LMPopArguments, parseLMPopArguments } from './LMPOP'; export default { - FIRST_KEY_INDEX: 3, IS_READ_ONLY: false, - transformArguments( - timeout: number, - ...args: LMPopArguments - ) { - return transformLMPopArguments( - ['BLMPOP', timeout.toString()], - ...args - ); - }, + parseCommand(parser: CommandParser, timeout: number, ...args: LMPopArguments) { + parser.push('BLMPOP', timeout.toString()); + parseLMPopArguments(parser, ...args); + }, transformReply: LMPOP.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BLPOP.spec.ts b/packages/client/lib/commands/BLPOP.spec.ts index 4bcc08d0fc9..1bb53a774b7 100644 --- a/packages/client/lib/commands/BLPOP.spec.ts +++ b/packages/client/lib/commands/BLPOP.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; import BLPOP from './BLPOP'; +import { parseArgs } from './generic-transformers'; describe('BLPOP', () => { describe('transformArguments', () => { it('single', () => { assert.deepEqual( - BLPOP.transformArguments('key', 0), + parseArgs(BLPOP, 'key', 0), ['BLPOP', 'key', '0'] ); }); it('multiple', () => { assert.deepEqual( - BLPOP.transformArguments(['1', '2'], 0), + parseArgs(BLPOP, ['1', '2'], 0), ['BLPOP', '1', '2', '0'] ); }); diff --git a/packages/client/lib/commands/BLPOP.ts b/packages/client/lib/commands/BLPOP.ts index c9f8b4775eb..aa0b30e768e 100644 --- a/packages/client/lib/commands/BLPOP.ts +++ b/packages/client/lib/commands/BLPOP.ts @@ -1,16 +1,13 @@ +import { CommandParser } from '../client/parser'; import { UnwrapReply, NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( - key: RedisVariadicArgument, - timeout: number - ) { - const args = pushVariadicArguments(['BLPOP'], key); - args.push(timeout.toString()); - return args; + parseCommand(parser: CommandParser, key: RedisVariadicArgument, timeout: number) { + parser.push('BLPOP'); + parser.pushKeys(key); + parser.push(timeout.toString()); }, transformReply(reply: UnwrapReply>) { if (reply === null) return null; diff --git a/packages/client/lib/commands/BRPOP.spec.ts b/packages/client/lib/commands/BRPOP.spec.ts index 21631d763f4..de23bb34a92 100644 --- a/packages/client/lib/commands/BRPOP.spec.ts +++ b/packages/client/lib/commands/BRPOP.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; import BRPOP from './BRPOP'; +import { parseArgs } from './generic-transformers'; describe('BRPOP', () => { describe('transformArguments', () => { it('single', () => { assert.deepEqual( - BRPOP.transformArguments('key', 0), + parseArgs(BRPOP, 'key', 0), ['BRPOP', 'key', '0'] ); }); it('multiple', () => { assert.deepEqual( - BRPOP.transformArguments(['1', '2'], 0), + parseArgs(BRPOP, ['1', '2'], 0), ['BRPOP', '1', '2', '0'] ); }); diff --git a/packages/client/lib/commands/BRPOP.ts b/packages/client/lib/commands/BRPOP.ts index f9c8aaa5037..401a951556d 100644 --- a/packages/client/lib/commands/BRPOP.ts +++ b/packages/client/lib/commands/BRPOP.ts @@ -1,17 +1,14 @@ +import { CommandParser } from '../client/parser'; import { Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; import BLPOP from './BLPOP'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( - key: RedisVariadicArgument, - timeout: number - ) { - const args = pushVariadicArguments(['BRPOP'], key); - args.push(timeout.toString()); - return args; + parseCommand(parser: CommandParser, key: RedisVariadicArgument, timeout: number) { + parser.push('BRPOP'); + parser.pushKeys(key); + parser.push(timeout.toString()); }, transformReply: BLPOP.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BRPOPLPUSH.spec.ts b/packages/client/lib/commands/BRPOPLPUSH.spec.ts index 1f6dc48bfea..6c2a2a2c900 100644 --- a/packages/client/lib/commands/BRPOPLPUSH.spec.ts +++ b/packages/client/lib/commands/BRPOPLPUSH.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; import BRPOPLPUSH from './BRPOPLPUSH'; +import { parseArgs } from './generic-transformers'; describe('BRPOPLPUSH', () => { it('transformArguments', () => { assert.deepEqual( - BRPOPLPUSH.transformArguments('source', 'destination', 0), + parseArgs(BRPOPLPUSH, 'source', 'destination', 0), ['BRPOPLPUSH', 'source', 'destination', '0'] ); }); diff --git a/packages/client/lib/commands/BRPOPLPUSH.ts b/packages/client/lib/commands/BRPOPLPUSH.ts index d342ea75725..72f63a1c1e5 100644 --- a/packages/client/lib/commands/BRPOPLPUSH.ts +++ b/packages/client/lib/commands/BRPOPLPUSH.ts @@ -1,14 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( - source: RedisArgument, - destination: RedisArgument, - timeout: number - ) { - return ['BRPOPLPUSH', source, destination, timeout.toString()]; + parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, timeout: number) { + parser.push('BRPOPLPUSH'); + parser.pushKeys([source, destination]); + parser.push(timeout.toString()); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BZMPOP.spec.ts b/packages/client/lib/commands/BZMPOP.spec.ts index 554e6898d62..8b082a214ee 100644 --- a/packages/client/lib/commands/BZMPOP.spec.ts +++ b/packages/client/lib/commands/BZMPOP.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; import BZMPOP from './BZMPOP'; +import { parseArgs } from './generic-transformers'; describe('BZMPOP', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,14 +9,14 @@ describe('BZMPOP', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - BZMPOP.transformArguments(0, 'key', 'MIN'), + parseArgs(BZMPOP, 0, 'key', 'MIN'), ['BZMPOP', '0', '1', 'key', 'MIN'] ); }); it('with COUNT', () => { assert.deepEqual( - BZMPOP.transformArguments(0, 'key', 'MIN', { + parseArgs(BZMPOP, 0, 'key', 'MIN', { COUNT: 2 }), ['BZMPOP', '0', '1', 'key', 'MIN', 'COUNT', '2'] diff --git a/packages/client/lib/commands/BZMPOP.ts b/packages/client/lib/commands/BZMPOP.ts index 030aa20c66b..98079b7a20d 100644 --- a/packages/client/lib/commands/BZMPOP.ts +++ b/packages/client/lib/commands/BZMPOP.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { Command } from '../RESP/types'; -import ZMPOP, { ZMPopArguments, transformZMPopArguments } from './ZMPOP'; +import ZMPOP, { parseZMPopArguments, ZMPopArguments } from './ZMPOP'; export default { - FIRST_KEY_INDEX: 3, IS_READ_ONLY: false, - transformArguments(timeout: number, ...args: ZMPopArguments) { - return transformZMPopArguments(['BZMPOP', timeout.toString()], ...args); + parseCommand(parser: CommandParser, timeout: number, ...args: ZMPopArguments) { + parser.push('BZMPOP', timeout.toString()); + parseZMPopArguments(parser, ...args); }, transformReply: ZMPOP.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/BZPOPMAX.spec.ts b/packages/client/lib/commands/BZPOPMAX.spec.ts index 1f0a4d44f07..fbf60862327 100644 --- a/packages/client/lib/commands/BZPOPMAX.spec.ts +++ b/packages/client/lib/commands/BZPOPMAX.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; import BZPOPMAX from './BZPOPMAX'; +import { parseArgs } from './generic-transformers'; describe('BZPOPMAX', () => { describe('transformArguments', () => { it('single', () => { assert.deepEqual( - BZPOPMAX.transformArguments('key', 0), + parseArgs(BZPOPMAX, 'key', 0), ['BZPOPMAX', 'key', '0'] ); }); it('multiple', () => { assert.deepEqual( - BZPOPMAX.transformArguments(['1', '2'], 0), + parseArgs(BZPOPMAX, ['1', '2'], 0), ['BZPOPMAX', '1', '2', '0'] ); }); diff --git a/packages/client/lib/commands/BZPOPMAX.ts b/packages/client/lib/commands/BZPOPMAX.ts index 792a5592574..1a5159269e9 100644 --- a/packages/client/lib/commands/BZPOPMAX.ts +++ b/packages/client/lib/commands/BZPOPMAX.ts @@ -1,23 +1,13 @@ -import { RedisArgument, NullReply, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments, transformDoubleReply } from './generic-transformers'; - -export function transformBZPopArguments( - command: RedisArgument, - key: RedisVariadicArgument, - timeout: number -) { - const args = pushVariadicArguments([command], key); - args.push(timeout.toString()); - return args; -} - -export type BZPopArguments = typeof transformBZPopArguments extends (_: any, ...args: infer T) => any ? T : never; +import { CommandParser } from '../client/parser'; +import { NullReply, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; +import { RedisVariadicArgument, transformDoubleReply } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(...args: BZPopArguments) { - return transformBZPopArguments('BZPOPMAX', ...args); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument, timeout: number) { + parser.push('BZPOPMAX'); + parser.pushKeys(keys); + parser.push(timeout.toString()); }, transformReply: { 2( diff --git a/packages/client/lib/commands/BZPOPMIN.spec.ts b/packages/client/lib/commands/BZPOPMIN.spec.ts index 7f39f7d1896..2f8cab8dedf 100644 --- a/packages/client/lib/commands/BZPOPMIN.spec.ts +++ b/packages/client/lib/commands/BZPOPMIN.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, BLOCKING_MIN_VALUE } from '../test-utils'; import BZPOPMIN from './BZPOPMIN'; +import { parseArgs } from './generic-transformers'; describe('BZPOPMIN', () => { describe('transformArguments', () => { it('single', () => { assert.deepEqual( - BZPOPMIN.transformArguments('key', 0), + parseArgs(BZPOPMIN, 'key', 0), ['BZPOPMIN', 'key', '0'] ); }); it('multiple', () => { assert.deepEqual( - BZPOPMIN.transformArguments(['1', '2'], 0), + parseArgs(BZPOPMIN, ['1', '2'], 0), ['BZPOPMIN', '1', '2', '0'] ); }); diff --git a/packages/client/lib/commands/BZPOPMIN.ts b/packages/client/lib/commands/BZPOPMIN.ts index f27e623528e..9dc4c47e13d 100644 --- a/packages/client/lib/commands/BZPOPMIN.ts +++ b/packages/client/lib/commands/BZPOPMIN.ts @@ -1,11 +1,14 @@ +import { CommandParser } from '../client/parser'; import { Command } from '../RESP/types'; -import BZPOPMAX, { BZPopArguments, transformBZPopArguments } from './BZPOPMAX'; +import { RedisVariadicArgument } from './generic-transformers'; +import BZPOPMAX from './BZPOPMAX'; export default { - FIRST_KEY_INDEX: BZPOPMAX.FIRST_KEY_INDEX, IS_READ_ONLY: BZPOPMAX.IS_READ_ONLY, - transformArguments(...args: BZPopArguments) { - return transformBZPopArguments('BZPOPMIN', ...args); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument, timeout: number) { + parser.push('BZPOPMIN'); + parser.pushKeys(keys); + parser.push(timeout.toString()); }, transformReply: BZPOPMAX.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_CACHING.spec.ts b/packages/client/lib/commands/CLIENT_CACHING.spec.ts index 34023f98922..ad3511b3e97 100644 --- a/packages/client/lib/commands/CLIENT_CACHING.spec.ts +++ b/packages/client/lib/commands/CLIENT_CACHING.spec.ts @@ -1,18 +1,19 @@ import { strict as assert } from 'node:assert'; import CLIENT_CACHING from './CLIENT_CACHING'; +import { parseArgs } from './generic-transformers'; describe('CLIENT CACHING', () => { describe('transformArguments', () => { it('true', () => { assert.deepEqual( - CLIENT_CACHING.transformArguments(true), + parseArgs(CLIENT_CACHING, true), ['CLIENT', 'CACHING', 'YES'] ); }); it('false', () => { assert.deepEqual( - CLIENT_CACHING.transformArguments(false), + parseArgs(CLIENT_CACHING, false), ['CLIENT', 'CACHING', 'NO'] ); }); diff --git a/packages/client/lib/commands/CLIENT_CACHING.ts b/packages/client/lib/commands/CLIENT_CACHING.ts index 505ae152f85..9987e49c99b 100644 --- a/packages/client/lib/commands/CLIENT_CACHING.ts +++ b/packages/client/lib/commands/CLIENT_CACHING.ts @@ -1,14 +1,15 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(value: boolean) { - return [ + parseCommand(parser: CommandParser, value: boolean) { + parser.push( 'CLIENT', 'CACHING', value ? 'YES' : 'NO' - ]; + ); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_GETNAME.spec.ts b/packages/client/lib/commands/CLIENT_GETNAME.spec.ts index 8975f1fee9c..5b0dfdb8437 100644 --- a/packages/client/lib/commands/CLIENT_GETNAME.spec.ts +++ b/packages/client/lib/commands/CLIENT_GETNAME.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLIENT_GETNAME from './CLIENT_GETNAME'; +import { parseArgs } from './generic-transformers'; describe('CLIENT GETNAME', () => { it('transformArguments', () => { assert.deepEqual( - CLIENT_GETNAME.transformArguments(), + parseArgs(CLIENT_GETNAME), ['CLIENT', 'GETNAME'] ); }); diff --git a/packages/client/lib/commands/CLIENT_GETNAME.ts b/packages/client/lib/commands/CLIENT_GETNAME.ts index c46b576407b..2e18c43cd5f 100644 --- a/packages/client/lib/commands/CLIENT_GETNAME.ts +++ b/packages/client/lib/commands/CLIENT_GETNAME.ts @@ -1,13 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return [ - 'CLIENT', - 'GETNAME' - ]; + parseCommand(parser: CommandParser) { + parser.push('CLIENT', 'GETNAME'); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts b/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts index 5cfedf2a4e7..a7c375fec26 100644 --- a/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts +++ b/packages/client/lib/commands/CLIENT_GETREDIR.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import CLIENT_GETREDIR from './CLIENT_GETREDIR'; +import { parseArgs } from './generic-transformers'; describe('CLIENT GETREDIR', () => { it('transformArguments', () => { assert.deepEqual( - CLIENT_GETREDIR.transformArguments(), + parseArgs(CLIENT_GETREDIR), ['CLIENT', 'GETREDIR'] ); }); diff --git a/packages/client/lib/commands/CLIENT_GETREDIR.ts b/packages/client/lib/commands/CLIENT_GETREDIR.ts index ae0b601b4e8..80cc6418dab 100644 --- a/packages/client/lib/commands/CLIENT_GETREDIR.ts +++ b/packages/client/lib/commands/CLIENT_GETREDIR.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLIENT', 'GETREDIR'] + parseCommand(parser: CommandParser) { + parser.push('CLIENT', 'GETREDIR'); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_ID.spec.ts b/packages/client/lib/commands/CLIENT_ID.spec.ts index 7b51e6bd930..51b308adf2c 100644 --- a/packages/client/lib/commands/CLIENT_ID.spec.ts +++ b/packages/client/lib/commands/CLIENT_ID.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLIENT_ID from './CLIENT_ID'; +import { parseArgs } from './generic-transformers'; describe('CLIENT ID', () => { it('transformArguments', () => { assert.deepEqual( - CLIENT_ID.transformArguments(), + parseArgs(CLIENT_ID), ['CLIENT', 'ID'] ); }); diff --git a/packages/client/lib/commands/CLIENT_ID.ts b/packages/client/lib/commands/CLIENT_ID.ts index 165ab1931eb..da58786ec3c 100644 --- a/packages/client/lib/commands/CLIENT_ID.ts +++ b/packages/client/lib/commands/CLIENT_ID.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLIENT', 'ID']; + parseCommand(parser: CommandParser) { + parser.push('CLIENT', 'ID'); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_INFO.spec.ts b/packages/client/lib/commands/CLIENT_INFO.spec.ts index 0aba384aa3a..50345a46ce3 100644 --- a/packages/client/lib/commands/CLIENT_INFO.spec.ts +++ b/packages/client/lib/commands/CLIENT_INFO.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import CLIENT_INFO from './CLIENT_INFO'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; describe('CLIENT INFO', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - CLIENT_INFO.transformArguments(), + parseArgs(CLIENT_INFO), ['CLIENT', 'INFO'] ); }); diff --git a/packages/client/lib/commands/CLIENT_INFO.ts b/packages/client/lib/commands/CLIENT_INFO.ts index 88721e2f8b9..36dac175443 100644 --- a/packages/client/lib/commands/CLIENT_INFO.ts +++ b/packages/client/lib/commands/CLIENT_INFO.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { Command, VerbatimStringReply } from '../RESP/types'; export interface ClientInfoReply { @@ -56,10 +57,10 @@ export interface ClientInfoReply { const CLIENT_INFO_REGEX = /([^\s=]+)=([^\s]*)/g; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLIENT', 'INFO'] + parseCommand(parser: CommandParser) { + parser.push('CLIENT', 'INFO'); }, transformReply(rawReply: VerbatimStringReply) { const map: Record = {}; diff --git a/packages/client/lib/commands/CLIENT_KILL.spec.ts b/packages/client/lib/commands/CLIENT_KILL.spec.ts index 79254af41f9..5078a267516 100644 --- a/packages/client/lib/commands/CLIENT_KILL.spec.ts +++ b/packages/client/lib/commands/CLIENT_KILL.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import CLIENT_KILL, { CLIENT_KILL_FILTERS } from './CLIENT_KILL'; +import { parseArgs } from './generic-transformers'; describe('CLIENT KILL', () => { describe('transformArguments', () => { it('ADDRESS', () => { assert.deepEqual( - CLIENT_KILL.transformArguments({ + parseArgs(CLIENT_KILL, { filter: CLIENT_KILL_FILTERS.ADDRESS, address: 'ip:6379' }), @@ -15,7 +16,7 @@ describe('CLIENT KILL', () => { it('LOCAL_ADDRESS', () => { assert.deepEqual( - CLIENT_KILL.transformArguments({ + parseArgs(CLIENT_KILL, { filter: CLIENT_KILL_FILTERS.LOCAL_ADDRESS, localAddress: 'ip:6379' }), @@ -26,7 +27,7 @@ describe('CLIENT KILL', () => { describe('ID', () => { it('string', () => { assert.deepEqual( - CLIENT_KILL.transformArguments({ + parseArgs(CLIENT_KILL, { filter: CLIENT_KILL_FILTERS.ID, id: '1' }), @@ -36,7 +37,7 @@ describe('CLIENT KILL', () => { it('number', () => { assert.deepEqual( - CLIENT_KILL.transformArguments({ + parseArgs(CLIENT_KILL, { filter: CLIENT_KILL_FILTERS.ID, id: 1 }), @@ -47,7 +48,7 @@ describe('CLIENT KILL', () => { it('TYPE', () => { assert.deepEqual( - CLIENT_KILL.transformArguments({ + parseArgs(CLIENT_KILL, { filter: CLIENT_KILL_FILTERS.TYPE, type: 'master' }), @@ -57,7 +58,7 @@ describe('CLIENT KILL', () => { it('USER', () => { assert.deepEqual( - CLIENT_KILL.transformArguments({ + parseArgs(CLIENT_KILL, { filter: CLIENT_KILL_FILTERS.USER, username: 'username' }), @@ -67,7 +68,7 @@ describe('CLIENT KILL', () => { it('MAXAGE', () => { assert.deepEqual( - CLIENT_KILL.transformArguments({ + parseArgs(CLIENT_KILL, { filter: CLIENT_KILL_FILTERS.MAXAGE, maxAge: 10 }), @@ -78,14 +79,14 @@ describe('CLIENT KILL', () => { describe('SKIP_ME', () => { it('undefined', () => { assert.deepEqual( - CLIENT_KILL.transformArguments(CLIENT_KILL_FILTERS.SKIP_ME), + parseArgs(CLIENT_KILL, CLIENT_KILL_FILTERS.SKIP_ME), ['CLIENT', 'KILL', 'SKIPME'] ); }); it('true', () => { assert.deepEqual( - CLIENT_KILL.transformArguments({ + parseArgs(CLIENT_KILL, { filter: CLIENT_KILL_FILTERS.SKIP_ME, skipMe: true }), @@ -95,7 +96,7 @@ describe('CLIENT KILL', () => { it('false', () => { assert.deepEqual( - CLIENT_KILL.transformArguments({ + parseArgs(CLIENT_KILL, { filter: CLIENT_KILL_FILTERS.SKIP_ME, skipMe: false }), @@ -106,7 +107,7 @@ describe('CLIENT KILL', () => { it('TYPE & SKIP_ME', () => { assert.deepEqual( - CLIENT_KILL.transformArguments([ + parseArgs(CLIENT_KILL, [ { filter: CLIENT_KILL_FILTERS.TYPE, type: 'master' diff --git a/packages/client/lib/commands/CLIENT_KILL.ts b/packages/client/lib/commands/CLIENT_KILL.ts index c5eb5304c57..24f8f0873f1 100644 --- a/packages/client/lib/commands/CLIENT_KILL.ts +++ b/packages/client/lib/commands/CLIENT_KILL.ts @@ -1,4 +1,5 @@ -import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { CommandParser } from '../client/parser'; +import { NumberReply, Command } from '../RESP/types'; export const CLIENT_KILL_FILTERS = { ADDRESS: 'ADDR', @@ -47,43 +48,42 @@ export interface ClientKillMaxAge extends ClientKillFilterCommon) { - const args = ['CLIENT', 'KILL']; + parseCommand(parser: CommandParser, filters: ClientKillFilter | Array) { + parser.push('CLIENT', 'KILL'); if (Array.isArray(filters)) { for (const filter of filters) { - pushFilter(args, filter); + pushFilter(parser, filter); } } else { - pushFilter(args, filters); + pushFilter(parser, filters); } - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; -function pushFilter(args: Array, filter: ClientKillFilter): void { +function pushFilter(parser: CommandParser, filter: ClientKillFilter): void { if (filter === CLIENT_KILL_FILTERS.SKIP_ME) { - args.push('SKIPME'); + parser.push('SKIPME'); return; } - args.push(filter.filter); + parser.push(filter.filter); switch (filter.filter) { case CLIENT_KILL_FILTERS.ADDRESS: - args.push(filter.address); + parser.push(filter.address); break; case CLIENT_KILL_FILTERS.LOCAL_ADDRESS: - args.push(filter.localAddress); + parser.push(filter.localAddress); break; case CLIENT_KILL_FILTERS.ID: - args.push( + parser.push( typeof filter.id === 'number' ? filter.id.toString() : filter.id @@ -91,19 +91,19 @@ function pushFilter(args: Array, filter: ClientKillFilter): void break; case CLIENT_KILL_FILTERS.TYPE: - args.push(filter.type); + parser.push(filter.type); break; case CLIENT_KILL_FILTERS.USER: - args.push(filter.username); + parser.push(filter.username); break; case CLIENT_KILL_FILTERS.SKIP_ME: - args.push(filter.skipMe ? 'yes' : 'no'); + parser.push(filter.skipMe ? 'yes' : 'no'); break; case CLIENT_KILL_FILTERS.MAXAGE: - args.push(filter.maxAge.toString()); + parser.push(filter.maxAge.toString()); break; } } diff --git a/packages/client/lib/commands/CLIENT_LIST.spec.ts b/packages/client/lib/commands/CLIENT_LIST.spec.ts index e967a8dc0ff..34709c5f14f 100644 --- a/packages/client/lib/commands/CLIENT_LIST.spec.ts +++ b/packages/client/lib/commands/CLIENT_LIST.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import CLIENT_LIST from './CLIENT_LIST'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; describe('CLIENT LIST', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - CLIENT_LIST.transformArguments(), + parseArgs(CLIENT_LIST), ['CLIENT', 'LIST'] ); }); it('with TYPE', () => { assert.deepEqual( - CLIENT_LIST.transformArguments({ + parseArgs(CLIENT_LIST, { TYPE: 'NORMAL' }), ['CLIENT', 'LIST', 'TYPE', 'NORMAL'] @@ -22,7 +23,7 @@ describe('CLIENT LIST', () => { it('with ID', () => { assert.deepEqual( - CLIENT_LIST.transformArguments({ + parseArgs(CLIENT_LIST, { ID: ['1', '2'] }), ['CLIENT', 'LIST', 'ID', '1', '2'] diff --git a/packages/client/lib/commands/CLIENT_LIST.ts b/packages/client/lib/commands/CLIENT_LIST.ts index dc43fb8855d..1e7f3d9ab40 100644 --- a/packages/client/lib/commands/CLIENT_LIST.ts +++ b/packages/client/lib/commands/CLIENT_LIST.ts @@ -1,5 +1,5 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types'; -import { pushVariadicArguments } from './generic-transformers'; import CLIENT_INFO, { ClientInfoReply } from './CLIENT_INFO'; export interface ListFilterType { @@ -15,21 +15,18 @@ export interface ListFilterId { export type ListFilter = ListFilterType | ListFilterId; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(filter?: ListFilter) { - let args: Array = ['CLIENT', 'LIST']; - + parseCommand(parser: CommandParser, filter?: ListFilter) { + parser.push('CLIENT', 'LIST'); if (filter) { if (filter.TYPE !== undefined) { - args.push('TYPE', filter.TYPE); + parser.push('TYPE', filter.TYPE); } else { - args.push('ID'); - args = pushVariadicArguments(args, filter.ID); + parser.push('ID'); + parser.pushVariadic(filter.ID); } } - - return args; }, transformReply(rawReply: VerbatimStringReply): Array { const split = rawReply.toString().split('\n'), diff --git a/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts b/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts index 5de4dfd7604..50afd413492 100644 --- a/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts +++ b/packages/client/lib/commands/CLIENT_NO-EVICT.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLIENT_NO_EVICT from './CLIENT_NO-EVICT'; +import { parseArgs } from './generic-transformers'; describe('CLIENT NO-EVICT', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,14 +9,14 @@ describe('CLIENT NO-EVICT', () => { describe('transformArguments', () => { it('true', () => { assert.deepEqual( - CLIENT_NO_EVICT.transformArguments(true), + parseArgs(CLIENT_NO_EVICT, true), ['CLIENT', 'NO-EVICT', 'ON'] ); }); it('false', () => { assert.deepEqual( - CLIENT_NO_EVICT.transformArguments(false), + parseArgs(CLIENT_NO_EVICT, false), ['CLIENT', 'NO-EVICT', 'OFF'] ); }); diff --git a/packages/client/lib/commands/CLIENT_NO-EVICT.ts b/packages/client/lib/commands/CLIENT_NO-EVICT.ts index 82aa50074ba..de2f65270e2 100644 --- a/packages/client/lib/commands/CLIENT_NO-EVICT.ts +++ b/packages/client/lib/commands/CLIENT_NO-EVICT.ts @@ -1,14 +1,15 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(value: boolean) { - return [ + parseCommand(parser: CommandParser, value: boolean) { + parser.push( 'CLIENT', 'NO-EVICT', value ? 'ON' : 'OFF' - ]; + ); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts b/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts index e58c22d9c6e..ec5c9f18ae9 100644 --- a/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts +++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLIENT_NO_TOUCH from './CLIENT_NO-TOUCH'; +import { parseArgs } from './generic-transformers'; describe('CLIENT NO-TOUCH', () => { testUtils.isVersionGreaterThanHook([7, 2]); @@ -8,14 +9,14 @@ describe('CLIENT NO-TOUCH', () => { describe('transformArguments', () => { it('true', () => { assert.deepEqual( - CLIENT_NO_TOUCH.transformArguments(true), + parseArgs(CLIENT_NO_TOUCH, true), ['CLIENT', 'NO-TOUCH', 'ON'] ); }); it('false', () => { assert.deepEqual( - CLIENT_NO_TOUCH.transformArguments(false), + parseArgs(CLIENT_NO_TOUCH, false), ['CLIENT', 'NO-TOUCH', 'OFF'] ); }); diff --git a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts index a6fc5eb1765..8c6deff4af5 100644 --- a/packages/client/lib/commands/CLIENT_NO-TOUCH.ts +++ b/packages/client/lib/commands/CLIENT_NO-TOUCH.ts @@ -1,14 +1,15 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(value: boolean) { - return [ + parseCommand(parser: CommandParser, value: boolean) { + parser.push( 'CLIENT', 'NO-TOUCH', value ? 'ON' : 'OFF' - ]; + ); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_PAUSE.spec.ts b/packages/client/lib/commands/CLIENT_PAUSE.spec.ts index a30f9075072..e213433afbe 100644 --- a/packages/client/lib/commands/CLIENT_PAUSE.spec.ts +++ b/packages/client/lib/commands/CLIENT_PAUSE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLIENT_PAUSE from './CLIENT_PAUSE'; +import { parseArgs } from './generic-transformers'; describe('CLIENT PAUSE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - CLIENT_PAUSE.transformArguments(0), + parseArgs(CLIENT_PAUSE, 0), ['CLIENT', 'PAUSE', '0'] ); }); it('with mode', () => { assert.deepEqual( - CLIENT_PAUSE.transformArguments(0, 'ALL'), + parseArgs(CLIENT_PAUSE, 0, 'ALL'), ['CLIENT', 'PAUSE', '0', 'ALL'] ); }); diff --git a/packages/client/lib/commands/CLIENT_PAUSE.ts b/packages/client/lib/commands/CLIENT_PAUSE.ts index 87b4177ed8c..ae6e4376364 100644 --- a/packages/client/lib/commands/CLIENT_PAUSE.ts +++ b/packages/client/lib/commands/CLIENT_PAUSE.ts @@ -1,20 +1,14 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(timeout: number, mode?: 'WRITE' | 'ALL') { - const args = [ - 'CLIENT', - 'PAUSE', - timeout.toString() - ]; - + parseCommand(parser: CommandParser, timeout: number, mode?: 'WRITE' | 'ALL') { + parser.push('CLIENT', 'PAUSE', timeout.toString()); if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_SETNAME.spec.ts b/packages/client/lib/commands/CLIENT_SETNAME.spec.ts index 8e6b914791d..b2b339c3d19 100644 --- a/packages/client/lib/commands/CLIENT_SETNAME.spec.ts +++ b/packages/client/lib/commands/CLIENT_SETNAME.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLIENT_SETNAME from './CLIENT_SETNAME'; +import { parseArgs } from './generic-transformers'; describe('CLIENT SETNAME', () => { it('transformArguments', () => { assert.deepEqual( - CLIENT_SETNAME.transformArguments('name'), + parseArgs(CLIENT_SETNAME, 'name'), ['CLIENT', 'SETNAME', 'name'] ); }); diff --git a/packages/client/lib/commands/CLIENT_SETNAME.ts b/packages/client/lib/commands/CLIENT_SETNAME.ts index e2e2a921958..335891e8308 100644 --- a/packages/client/lib/commands/CLIENT_SETNAME.ts +++ b/packages/client/lib/commands/CLIENT_SETNAME.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(name: RedisArgument) { - return ['CLIENT', 'SETNAME', name]; + parseCommand(parser: CommandParser, name: RedisArgument) { + parser.push('CLIENT', 'SETNAME', name); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_TRACKING.spec.ts b/packages/client/lib/commands/CLIENT_TRACKING.spec.ts index 98fe091fb1b..032725635ee 100644 --- a/packages/client/lib/commands/CLIENT_TRACKING.spec.ts +++ b/packages/client/lib/commands/CLIENT_TRACKING.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLIENT_TRACKING from './CLIENT_TRACKING'; +import { parseArgs } from './generic-transformers'; describe('CLIENT TRACKING', () => { testUtils.isVersionGreaterThanHook([6]); @@ -9,14 +10,14 @@ describe('CLIENT TRACKING', () => { describe('true', () => { it('simple', () => { assert.deepEqual( - CLIENT_TRACKING.transformArguments(true), + parseArgs(CLIENT_TRACKING, true), ['CLIENT', 'TRACKING', 'ON'] ); }); it('with REDIRECT', () => { assert.deepEqual( - CLIENT_TRACKING.transformArguments(true, { + parseArgs(CLIENT_TRACKING, true, { REDIRECT: 1 }), ['CLIENT', 'TRACKING', 'ON', 'REDIRECT', '1'] @@ -26,7 +27,7 @@ describe('CLIENT TRACKING', () => { describe('with BCAST', () => { it('simple', () => { assert.deepEqual( - CLIENT_TRACKING.transformArguments(true, { + parseArgs(CLIENT_TRACKING, true, { BCAST: true }), ['CLIENT', 'TRACKING', 'ON', 'BCAST'] @@ -36,7 +37,7 @@ describe('CLIENT TRACKING', () => { describe('with PREFIX', () => { it('string', () => { assert.deepEqual( - CLIENT_TRACKING.transformArguments(true, { + parseArgs(CLIENT_TRACKING, true, { BCAST: true, PREFIX: 'prefix' }), @@ -46,7 +47,7 @@ describe('CLIENT TRACKING', () => { it('array', () => { assert.deepEqual( - CLIENT_TRACKING.transformArguments(true, { + parseArgs(CLIENT_TRACKING, true, { BCAST: true, PREFIX: ['1', '2'] }), @@ -58,7 +59,7 @@ describe('CLIENT TRACKING', () => { it('with OPTIN', () => { assert.deepEqual( - CLIENT_TRACKING.transformArguments(true, { + parseArgs(CLIENT_TRACKING, true, { OPTIN: true }), ['CLIENT', 'TRACKING', 'ON', 'OPTIN'] @@ -67,7 +68,7 @@ describe('CLIENT TRACKING', () => { it('with OPTOUT', () => { assert.deepEqual( - CLIENT_TRACKING.transformArguments(true, { + parseArgs(CLIENT_TRACKING, true, { OPTOUT: true }), ['CLIENT', 'TRACKING', 'ON', 'OPTOUT'] @@ -76,7 +77,7 @@ describe('CLIENT TRACKING', () => { it('with NOLOOP', () => { assert.deepEqual( - CLIENT_TRACKING.transformArguments(true, { + parseArgs(CLIENT_TRACKING, true, { NOLOOP: true }), ['CLIENT', 'TRACKING', 'ON', 'NOLOOP'] @@ -86,7 +87,7 @@ describe('CLIENT TRACKING', () => { it('false', () => { assert.deepEqual( - CLIENT_TRACKING.transformArguments(false), + parseArgs(CLIENT_TRACKING, false), ['CLIENT', 'TRACKING', 'OFF'] ); }); diff --git a/packages/client/lib/commands/CLIENT_TRACKING.ts b/packages/client/lib/commands/CLIENT_TRACKING.ts index a783ce35894..df70a3705f9 100644 --- a/packages/client/lib/commands/CLIENT_TRACKING.ts +++ b/packages/client/lib/commands/CLIENT_TRACKING.ts @@ -1,4 +1,5 @@ -import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; +import { CommandParser } from '../client/parser'; +import { SimpleStringReply, Command } from '../RESP/types'; import { RedisVariadicArgument } from './generic-transformers'; interface CommonOptions { @@ -26,50 +27,49 @@ export type ClientTrackingOptions = CommonOptions & ( ); export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, mode: M, options?: M extends true ? ClientTrackingOptions : never ) { - const args: Array = [ + parser.push( 'CLIENT', 'TRACKING', mode ? 'ON' : 'OFF' - ]; + ); if (mode) { if (options?.REDIRECT) { - args.push( + parser.push( 'REDIRECT', options.REDIRECT.toString() ); } if (isBroadcast(options)) { - args.push('BCAST'); + parser.push('BCAST'); if (options?.PREFIX) { if (Array.isArray(options.PREFIX)) { for (const prefix of options.PREFIX) { - args.push('PREFIX', prefix); + parser.push('PREFIX', prefix); } } else { - args.push('PREFIX', options.PREFIX); + parser.push('PREFIX', options.PREFIX); } } } else if (isOptIn(options)) { - args.push('OPTIN'); + parser.push('OPTIN'); } else if (isOptOut(options)) { - args.push('OPTOUT'); + parser.push('OPTOUT'); } if (options?.NOLOOP) { - args.push('NOLOOP'); + parser.push('NOLOOP'); } } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts b/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts index 1cefbd27d53..d776519df22 100644 --- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts +++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLIENT_TRACKINGINFO from './CLIENT_TRACKINGINFO'; +import { parseArgs } from './generic-transformers'; describe('CLIENT TRACKINGINFO', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - CLIENT_TRACKINGINFO.transformArguments(), + parseArgs(CLIENT_TRACKINGINFO), ['CLIENT', 'TRACKINGINFO'] ); }); diff --git a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts index d969ba0219e..fe6e090455c 100644 --- a/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts +++ b/packages/client/lib/commands/CLIENT_TRACKINGINFO.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { TuplesToMapReply, BlobStringReply, SetReply, NumberReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; type TrackingInfo = TuplesToMapReply<[ @@ -7,10 +8,10 @@ type TrackingInfo = TuplesToMapReply<[ ]>; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLIENT', 'TRACKINGINFO']; + parseCommand(parser: CommandParser) { + parser.push('CLIENT', 'TRACKINGINFO'); }, transformReply: { 2: (reply: UnwrapReply>) => ({ diff --git a/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts b/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts index bddf3ca0f02..0b58cf6517e 100644 --- a/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts +++ b/packages/client/lib/commands/CLIENT_UNPAUSE.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLIENT_UNPAUSE from './CLIENT_UNPAUSE'; +import { parseArgs } from './generic-transformers'; describe('CLIENT UNPAUSE', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - CLIENT_UNPAUSE.transformArguments(), + parseArgs(CLIENT_UNPAUSE), ['CLIENT', 'UNPAUSE'] ); }); diff --git a/packages/client/lib/commands/CLIENT_UNPAUSE.ts b/packages/client/lib/commands/CLIENT_UNPAUSE.ts index 9da0a9a8bbe..c202e50a5df 100644 --- a/packages/client/lib/commands/CLIENT_UNPAUSE.ts +++ b/packages/client/lib/commands/CLIENT_UNPAUSE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLIENT', 'UNPAUSE']; + parseCommand(parser: CommandParser) { + parser.push('CLIENT', 'UNPAUSE'); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts index 56f7b2a85e7..4a9b1839bb4 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.spec.ts @@ -1,18 +1,19 @@ import { strict as assert } from 'node:assert'; import CLUSTER_ADDSLOTS from './CLUSTER_ADDSLOTS'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER ADDSLOTS', () => { describe('transformArguments', () => { it('single', () => { assert.deepEqual( - CLUSTER_ADDSLOTS.transformArguments(0), + parseArgs(CLUSTER_ADDSLOTS, 0), ['CLUSTER', 'ADDSLOTS', '0'] ); }); it('multiple', () => { assert.deepEqual( - CLUSTER_ADDSLOTS.transformArguments([0, 1]), + parseArgs(CLUSTER_ADDSLOTS, [0, 1]), ['CLUSTER', 'ADDSLOTS', '0', '1'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts index dc42c2f13e8..0f5c4513d1d 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTS.ts @@ -1,14 +1,12 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; -import { pushVariadicNumberArguments } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(slots: number | Array) { - return pushVariadicNumberArguments( - ['CLUSTER', 'ADDSLOTS'], - slots - ); + parseCommand(parser: CommandParser, slots: number | Array) { + parser.push('CLUSTER', 'ADDSLOTS'); + parser.pushVariadicNumber(slots); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts index 6af6f586e99..40706968f93 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; import CLUSTER_ADDSLOTSRANGE from './CLUSTER_ADDSLOTSRANGE'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER ADDSLOTSRANGE', () => { testUtils.isVersionGreaterThanHook([7, 0]); @@ -8,7 +9,7 @@ describe('CLUSTER ADDSLOTSRANGE', () => { describe('transformArguments', () => { it('single', () => { assert.deepEqual( - CLUSTER_ADDSLOTSRANGE.transformArguments({ + parseArgs(CLUSTER_ADDSLOTSRANGE, { start: 0, end: 1 }), @@ -18,7 +19,7 @@ describe('CLUSTER ADDSLOTSRANGE', () => { it('multiple', () => { assert.deepEqual( - CLUSTER_ADDSLOTSRANGE.transformArguments([{ + parseArgs(CLUSTER_ADDSLOTSRANGE, [{ start: 0, end: 1 }, { diff --git a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts index 5cf649a30da..40780731981 100644 --- a/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_ADDSLOTSRANGE.ts @@ -1,14 +1,13 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; -import { pushSlotRangesArguments, SlotRange } from './generic-transformers'; +import { parseSlotRangesArguments, SlotRange } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(ranges: SlotRange | Array) { - return pushSlotRangesArguments( - ['CLUSTER', 'ADDSLOTSRANGE'], - ranges - ); + parseCommand(parser: CommandParser, ranges: SlotRange | Array) { + parser.push('CLUSTER', 'ADDSLOTSRANGE'); + parseSlotRangesArguments(parser, ranges); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts index d21bc47c5d0..f3ecde9f6a8 100644 --- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts +++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_BUMPEPOCH from './CLUSTER_BUMPEPOCH'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER BUMPEPOCH', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_BUMPEPOCH.transformArguments(), + parseArgs(CLUSTER_BUMPEPOCH), ['CLUSTER', 'BUMPEPOCH'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts index 94f7e3b56f9..04b62f85424 100644 --- a/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts +++ b/packages/client/lib/commands/CLUSTER_BUMPEPOCH.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLUSTER', 'BUMPEPOCH']; + parseCommand(parser: CommandParser) { + parser.push('CLUSTER', 'BUMPEPOCH'); }, transformReply: undefined as unknown as () => SimpleStringReply<'BUMPED' | 'STILL'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts index 93c2aca7804..06a901ef301 100644 --- a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_COUNT_FAILURE_REPORTS from './CLUSTER_COUNT-FAILURE-REPORTS'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER COUNT-FAILURE-REPORTS', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_COUNT_FAILURE_REPORTS.transformArguments('0'), + parseArgs(CLUSTER_COUNT_FAILURE_REPORTS, '0'), ['CLUSTER', 'COUNT-FAILURE-REPORTS', '0'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts index a005694713d..0ac311f7ecd 100644 --- a/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts +++ b/packages/client/lib/commands/CLUSTER_COUNT-FAILURE-REPORTS.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(nodeId: RedisArgument) { - return ['CLUSTER', 'COUNT-FAILURE-REPORTS', nodeId]; + parseCommand(parser: CommandParser, nodeId: RedisArgument) { + parser.push('CLUSTER', 'COUNT-FAILURE-REPORTS', nodeId); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts index 180a120e153..52848409465 100644 --- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_COUNTKEYSINSLOT from './CLUSTER_COUNTKEYSINSLOT'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER COUNTKEYSINSLOT', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_COUNTKEYSINSLOT.transformArguments(0), + parseArgs(CLUSTER_COUNTKEYSINSLOT, 0), ['CLUSTER', 'COUNTKEYSINSLOT', '0'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts index 61f46230e89..63b4a8e02e2 100644 --- a/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_COUNTKEYSINSLOT.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(slot: number) { - return ['CLUSTER', 'COUNTKEYSINSLOT', slot.toString()]; + parseCommand(parser: CommandParser, slot: number) { + parser.push('CLUSTER', 'COUNTKEYSINSLOT', slot.toString()); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts index 59e40217b9c..2937fdd4d79 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.spec.ts @@ -1,18 +1,19 @@ import { strict as assert } from 'node:assert'; import CLUSTER_DELSLOTS from './CLUSTER_DELSLOTS'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER DELSLOTS', () => { describe('transformArguments', () => { it('single', () => { assert.deepEqual( - CLUSTER_DELSLOTS.transformArguments(0), + parseArgs(CLUSTER_DELSLOTS, 0), ['CLUSTER', 'DELSLOTS', '0'] ); }); it('multiple', () => { assert.deepEqual( - CLUSTER_DELSLOTS.transformArguments([0, 1]), + parseArgs(CLUSTER_DELSLOTS, [0, 1]), ['CLUSTER', 'DELSLOTS', '0', '1'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts index 6a6bbb76085..9be6e962a18 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTS.ts @@ -1,14 +1,12 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; -import { pushVariadicNumberArguments } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(slots: number | Array) { - return pushVariadicNumberArguments( - ['CLUSTER', 'DELSLOTS'], - slots - ); + parseCommand(parser: CommandParser, slots: number | Array) { + parser.push('CLUSTER', 'DELSLOTS'); + parser.pushVariadicNumber(slots); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts index 2615f394b87..6007421d11b 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import CLUSTER_DELSLOTSRANGE from './CLUSTER_DELSLOTSRANGE'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER DELSLOTSRANGE', () => { describe('transformArguments', () => { it('single', () => { assert.deepEqual( - CLUSTER_DELSLOTSRANGE.transformArguments({ + parseArgs(CLUSTER_DELSLOTSRANGE, { start: 0, end: 1 }), @@ -15,7 +16,7 @@ describe('CLUSTER DELSLOTSRANGE', () => { it('multiple', () => { assert.deepEqual( - CLUSTER_DELSLOTSRANGE.transformArguments([{ + parseArgs(CLUSTER_DELSLOTSRANGE, [{ start: 0, end: 1 }, { diff --git a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts index e28ca9c8405..64c04021ba1 100644 --- a/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts +++ b/packages/client/lib/commands/CLUSTER_DELSLOTSRANGE.ts @@ -1,14 +1,13 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; -import { pushSlotRangesArguments, SlotRange } from './generic-transformers'; +import { parseSlotRangesArguments, SlotRange } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(ranges: SlotRange | Array) { - return pushSlotRangesArguments( - ['CLUSTER', 'DELSLOTSRANGE'], - ranges - ); + parseCommand(parser:CommandParser, ranges: SlotRange | Array) { + parser.push('CLUSTER', 'DELSLOTSRANGE'); + parseSlotRangesArguments(parser, ranges); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts b/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts index ac18a9a7f8f..f8e4b986048 100644 --- a/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts +++ b/packages/client/lib/commands/CLUSTER_FAILOVER.spec.ts @@ -1,18 +1,19 @@ import { strict as assert } from 'node:assert'; import CLUSTER_FAILOVER, { FAILOVER_MODES } from './CLUSTER_FAILOVER'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER FAILOVER', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - CLUSTER_FAILOVER.transformArguments(), + parseArgs(CLUSTER_FAILOVER), ['CLUSTER', 'FAILOVER'] ); }); it('with mode', () => { assert.deepEqual( - CLUSTER_FAILOVER.transformArguments({ + parseArgs(CLUSTER_FAILOVER, { mode: FAILOVER_MODES.FORCE }), ['CLUSTER', 'FAILOVER', 'FORCE'] diff --git a/packages/client/lib/commands/CLUSTER_FAILOVER.ts b/packages/client/lib/commands/CLUSTER_FAILOVER.ts index 63f79a246ba..f74d65bd691 100644 --- a/packages/client/lib/commands/CLUSTER_FAILOVER.ts +++ b/packages/client/lib/commands/CLUSTER_FAILOVER.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export const FAILOVER_MODES = { @@ -12,16 +13,14 @@ export interface ClusterFailoverOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(options?: ClusterFailoverOptions) { - const args = ['CLUSTER', 'FAILOVER']; + parseCommand(parser:CommandParser, options?: ClusterFailoverOptions) { + parser.push('CLUSTER', 'FAILOVER'); if (options?.mode) { - args.push(options.mode); + parser.push(options.mode); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts index fbc4346136d..43701adfe6a 100644 --- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import CLUSTER_FLUSHSLOTS from './CLUSTER_FLUSHSLOTS'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER FLUSHSLOTS', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_FLUSHSLOTS.transformArguments(), + parseArgs(CLUSTER_FLUSHSLOTS), ['CLUSTER', 'FLUSHSLOTS'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts index 327ed7b7d17..dab22b2e740 100644 --- a/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_FLUSHSLOTS.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLUSTER', 'FLUSHSLOTS']; + parseCommand(parser: CommandParser) { + parser.push('CLUSTER', 'FLUSHSLOTS'); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_FORGET.spec.ts b/packages/client/lib/commands/CLUSTER_FORGET.spec.ts index a9a923b01ee..8d02374cf87 100644 --- a/packages/client/lib/commands/CLUSTER_FORGET.spec.ts +++ b/packages/client/lib/commands/CLUSTER_FORGET.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import CLUSTER_FORGET from './CLUSTER_FORGET'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER FORGET', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_FORGET.transformArguments('0'), + parseArgs(CLUSTER_FORGET, '0'), ['CLUSTER', 'FORGET', '0'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_FORGET.ts b/packages/client/lib/commands/CLUSTER_FORGET.ts index a51c039563b..2928c3e9075 100644 --- a/packages/client/lib/commands/CLUSTER_FORGET.ts +++ b/packages/client/lib/commands/CLUSTER_FORGET.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(nodeId: RedisArgument) { - return ['CLUSTER', 'FORGET', nodeId]; + parseCommand(parser: CommandParser, nodeId: RedisArgument) { + parser.push('CLUSTER', 'FORGET', nodeId); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts index f1a4e2c3bcc..468eecc74a9 100644 --- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_GETKEYSINSLOT from './CLUSTER_GETKEYSINSLOT'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER GETKEYSINSLOT', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_GETKEYSINSLOT.transformArguments(0, 10), + parseArgs(CLUSTER_GETKEYSINSLOT, 0, 10), ['CLUSTER', 'GETKEYSINSLOT', '0', '10'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts index c19cd225e04..2fd630ea1af 100644 --- a/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_GETKEYSINSLOT.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(slot: number, count: number) { - return ['CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()]; + parseCommand(parser: CommandParser, slot: number, count: number) { + parser.push('CLUSTER', 'GETKEYSINSLOT', slot.toString(), count.toString()); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_INFO.spec.ts b/packages/client/lib/commands/CLUSTER_INFO.spec.ts index f7c708663fc..01dafce8d53 100644 --- a/packages/client/lib/commands/CLUSTER_INFO.spec.ts +++ b/packages/client/lib/commands/CLUSTER_INFO.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_INFO from './CLUSTER_INFO'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER INFO', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_INFO.transformArguments(), + parseArgs(CLUSTER_INFO), ['CLUSTER', 'INFO'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_INFO.ts b/packages/client/lib/commands/CLUSTER_INFO.ts index 4605efbe81a..53140b38819 100644 --- a/packages/client/lib/commands/CLUSTER_INFO.ts +++ b/packages/client/lib/commands/CLUSTER_INFO.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { VerbatimStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLUSTER', 'INFO']; + parseCommand(parser: CommandParser) { + parser.push('CLUSTER', 'INFO'); }, transformReply: undefined as unknown as () => VerbatimStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts index d582c616cd1..188c403abb5 100644 --- a/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_KEYSLOT from './CLUSTER_KEYSLOT'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER KEYSLOT', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_KEYSLOT.transformArguments('key'), + parseArgs(CLUSTER_KEYSLOT, 'key'), ['CLUSTER', 'KEYSLOT', 'key'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts index 81e84430116..d81a14e1a96 100644 --- a/packages/client/lib/commands/CLUSTER_KEYSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_KEYSLOT.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { Command, NumberReply, RedisArgument } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['CLUSTER', 'KEYSLOT', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('CLUSTER', 'KEYSLOT', key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_LINKS.spec.ts b/packages/client/lib/commands/CLUSTER_LINKS.spec.ts index d94231634e0..609ecfd3da9 100644 --- a/packages/client/lib/commands/CLUSTER_LINKS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_LINKS.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_LINKS from './CLUSTER_LINKS'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER LINKS', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - CLUSTER_LINKS.transformArguments(), + parseArgs(CLUSTER_LINKS), ['CLUSTER', 'LINKS'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_LINKS.ts b/packages/client/lib/commands/CLUSTER_LINKS.ts index df83f3f7a11..e98f61e762b 100644 --- a/packages/client/lib/commands/CLUSTER_LINKS.ts +++ b/packages/client/lib/commands/CLUSTER_LINKS.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; type ClusterLinksReply = ArrayReply>; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLUSTER', 'LINKS']; + parseCommand(parser: CommandParser) { + parser.push('CLUSTER', 'LINKS'); }, transformReply: { 2: (reply: UnwrapReply>) => reply.map(link => { diff --git a/packages/client/lib/commands/CLUSTER_MEET.spec.ts b/packages/client/lib/commands/CLUSTER_MEET.spec.ts index 0b678f009f7..6c063f34e45 100644 --- a/packages/client/lib/commands/CLUSTER_MEET.spec.ts +++ b/packages/client/lib/commands/CLUSTER_MEET.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import CLUSTER_MEET from './CLUSTER_MEET'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER MEET', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_MEET.transformArguments('127.0.0.1', 6379), + parseArgs(CLUSTER_MEET, '127.0.0.1', 6379), ['CLUSTER', 'MEET', '127.0.0.1', '6379'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_MEET.ts b/packages/client/lib/commands/CLUSTER_MEET.ts index df72599d40b..804e5963d19 100644 --- a/packages/client/lib/commands/CLUSTER_MEET.ts +++ b/packages/client/lib/commands/CLUSTER_MEET.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(host: string, port: number) { - return ['CLUSTER', 'MEET', host, port.toString()]; + parseCommand(parser: CommandParser, host: string, port: number) { + parser.push('CLUSTER', 'MEET', host, port.toString()); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_MYID.spec.ts b/packages/client/lib/commands/CLUSTER_MYID.spec.ts index 74540e98ab7..78bb4495e3c 100644 --- a/packages/client/lib/commands/CLUSTER_MYID.spec.ts +++ b/packages/client/lib/commands/CLUSTER_MYID.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_MYID from './CLUSTER_MYID'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER MYID', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_MYID.transformArguments(), + parseArgs(CLUSTER_MYID), ['CLUSTER', 'MYID'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_MYID.ts b/packages/client/lib/commands/CLUSTER_MYID.ts index 73711b47ebb..2aae7cdd8e0 100644 --- a/packages/client/lib/commands/CLUSTER_MYID.ts +++ b/packages/client/lib/commands/CLUSTER_MYID.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLUSTER', 'MYID']; + parseCommand(parser: CommandParser) { + parser.push('CLUSTER', 'MYID'); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts b/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts index e64f2e3777a..6c2a61801bc 100644 --- a/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts +++ b/packages/client/lib/commands/CLUSTER_MYSHARDID.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_MYSHARDID from './CLUSTER_MYSHARDID'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER MYSHARDID', () => { testUtils.isVersionGreaterThanHook([7, 2]); it('transformArguments', () => { assert.deepEqual( - CLUSTER_MYSHARDID.transformArguments(), + parseArgs(CLUSTER_MYSHARDID), ['CLUSTER', 'MYSHARDID'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_MYSHARDID.ts b/packages/client/lib/commands/CLUSTER_MYSHARDID.ts index 0c38b61634f..ccde3ee249b 100644 --- a/packages/client/lib/commands/CLUSTER_MYSHARDID.ts +++ b/packages/client/lib/commands/CLUSTER_MYSHARDID.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLUSTER', 'MYSHARDID']; + parseCommand(parser: CommandParser) { + parser.push('CLUSTER', 'MYSHARDID'); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_NODES.spec.ts b/packages/client/lib/commands/CLUSTER_NODES.spec.ts index 99db17a23e6..a49996586b7 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.spec.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_NODES from './CLUSTER_NODES'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER NODES', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_NODES.transformArguments(), + parseArgs(CLUSTER_NODES), ['CLUSTER', 'NODES'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_NODES.ts b/packages/client/lib/commands/CLUSTER_NODES.ts index 64dd5056232..c8b59f88224 100644 --- a/packages/client/lib/commands/CLUSTER_NODES.ts +++ b/packages/client/lib/commands/CLUSTER_NODES.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { VerbatimStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLUSTER', 'NODES']; + parseCommand(parser: CommandParser) { + parser.push('CLUSTER', 'NODES'); }, transformReply: undefined as unknown as () => VerbatimStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts b/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts index 1a48f360885..11bf086bb66 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICAS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_REPLICAS from './CLUSTER_REPLICAS'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER REPLICAS', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_REPLICAS.transformArguments('0'), + parseArgs(CLUSTER_REPLICAS, '0'), ['CLUSTER', 'REPLICAS', '0'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_REPLICAS.ts b/packages/client/lib/commands/CLUSTER_REPLICAS.ts index 8e0fe2cdfd9..eb60e560b45 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICAS.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICAS.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(nodeId: RedisArgument) { - return ['CLUSTER', 'REPLICAS', nodeId]; + parseCommand(parser: CommandParser, nodeId: RedisArgument) { + parser.push('CLUSTER', 'REPLICAS', nodeId); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts b/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts index 80935385a88..3f130d360bf 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICATE.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import CLUSTER_REPLICATE from './CLUSTER_REPLICATE'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER REPLICATE', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_REPLICATE.transformArguments('0'), + parseArgs(CLUSTER_REPLICATE, '0'), ['CLUSTER', 'REPLICATE', '0'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_REPLICATE.ts b/packages/client/lib/commands/CLUSTER_REPLICATE.ts index 7431142024c..d7312ae108e 100644 --- a/packages/client/lib/commands/CLUSTER_REPLICATE.ts +++ b/packages/client/lib/commands/CLUSTER_REPLICATE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(nodeId: RedisArgument) { - return ['CLUSTER', 'REPLICATE', nodeId]; + parseCommand(parser: CommandParser, nodeId: RedisArgument) { + parser.push('CLUSTER', 'REPLICATE', nodeId); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_RESET.spec.ts b/packages/client/lib/commands/CLUSTER_RESET.spec.ts index 190bdaf69e1..1ef55e3f572 100644 --- a/packages/client/lib/commands/CLUSTER_RESET.spec.ts +++ b/packages/client/lib/commands/CLUSTER_RESET.spec.ts @@ -1,18 +1,19 @@ import { strict as assert } from 'node:assert'; import CLUSTER_RESET from './CLUSTER_RESET'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER RESET', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - CLUSTER_RESET.transformArguments(), + parseArgs(CLUSTER_RESET), ['CLUSTER', 'RESET'] ); }); it('with mode', () => { assert.deepEqual( - CLUSTER_RESET.transformArguments({ + parseArgs(CLUSTER_RESET, { mode: 'HARD' }), ['CLUSTER', 'RESET', 'HARD'] diff --git a/packages/client/lib/commands/CLUSTER_RESET.ts b/packages/client/lib/commands/CLUSTER_RESET.ts index 7aaac9d3b0d..2ba1a6eaf20 100644 --- a/packages/client/lib/commands/CLUSTER_RESET.ts +++ b/packages/client/lib/commands/CLUSTER_RESET.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export interface ClusterResetOptions { @@ -5,16 +6,14 @@ export interface ClusterResetOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(options?: ClusterResetOptions) { - const args = ['CLUSTER', 'RESET']; + parseCommand(parser: CommandParser, options?: ClusterResetOptions) { + parser.push('CLUSTER', 'RESET'); if (options?.mode) { - args.push(options.mode); + parser.push(options.mode); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts b/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts index ece8087e8e4..a0d317ffae4 100644 --- a/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SAVECONFIG.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_SAVECONFIG from './CLUSTER_SAVECONFIG'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER SAVECONFIG', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_SAVECONFIG.transformArguments(), + parseArgs(CLUSTER_SAVECONFIG), ['CLUSTER', 'SAVECONFIG'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts b/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts index 489ffd27e48..08da2a45b89 100644 --- a/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts +++ b/packages/client/lib/commands/CLUSTER_SAVECONFIG.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLUSTER', 'SAVECONFIG']; + parseCommand(parser: CommandParser) { + parser.push('CLUSTER', 'SAVECONFIG'); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts index 39cf026d0ef..fb02ee2fe65 100644 --- a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import CLUSTER_SET_CONFIG_EPOCH from './CLUSTER_SET-CONFIG-EPOCH'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER SET-CONFIG-EPOCH', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_SET_CONFIG_EPOCH.transformArguments(0), + parseArgs(CLUSTER_SET_CONFIG_EPOCH, 0), ['CLUSTER', 'SET-CONFIG-EPOCH', '0'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts index 2a650840c44..ba423df7fb7 100644 --- a/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts +++ b/packages/client/lib/commands/CLUSTER_SET-CONFIG-EPOCH.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(configEpoch: number) { - return ['CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString() ]; + parseCommand(parser: CommandParser, configEpoch: number) { + parser.push('CLUSTER', 'SET-CONFIG-EPOCH', configEpoch.toString()); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts b/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts index 7bce6d74b4a..fac496c3afb 100644 --- a/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SETSLOT.spec.ts @@ -1,18 +1,19 @@ import { strict as assert } from 'node:assert'; import CLUSTER_SETSLOT, { CLUSTER_SLOT_STATES } from './CLUSTER_SETSLOT'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER SETSLOT', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - CLUSTER_SETSLOT.transformArguments(0, CLUSTER_SLOT_STATES.IMPORTING), + parseArgs(CLUSTER_SETSLOT, 0, CLUSTER_SLOT_STATES.IMPORTING), ['CLUSTER', 'SETSLOT', '0', 'IMPORTING'] ); }); it('with nodeId', () => { assert.deepEqual( - CLUSTER_SETSLOT.transformArguments(0, CLUSTER_SLOT_STATES.IMPORTING, 'nodeId'), + parseArgs(CLUSTER_SETSLOT, 0, CLUSTER_SLOT_STATES.IMPORTING, 'nodeId'), ['CLUSTER', 'SETSLOT', '0', 'IMPORTING', 'nodeId'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_SETSLOT.ts b/packages/client/lib/commands/CLUSTER_SETSLOT.ts index ad04513688e..1f74316a3f3 100644 --- a/packages/client/lib/commands/CLUSTER_SETSLOT.ts +++ b/packages/client/lib/commands/CLUSTER_SETSLOT.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export const CLUSTER_SLOT_STATES = { @@ -10,16 +11,14 @@ export const CLUSTER_SLOT_STATES = { export type ClusterSlotState = typeof CLUSTER_SLOT_STATES[keyof typeof CLUSTER_SLOT_STATES]; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(slot: number, state: ClusterSlotState, nodeId?: RedisArgument) { - const args: Array = ['CLUSTER', 'SETSLOT', slot.toString(), state]; + parseCommand(parser: CommandParser, slot: number, state: ClusterSlotState, nodeId?: RedisArgument) { + parser.push('CLUSTER', 'SETSLOT', slot.toString(), state); if (nodeId) { - args.push(nodeId); + parser.push(nodeId); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts b/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts index 198dfdc6c1b..28879b036ae 100644 --- a/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts +++ b/packages/client/lib/commands/CLUSTER_SLOTS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLUSTER_SLOTS from './CLUSTER_SLOTS'; +import { parseArgs } from './generic-transformers'; describe('CLUSTER SLOTS', () => { it('transformArguments', () => { assert.deepEqual( - CLUSTER_SLOTS.transformArguments(), + parseArgs(CLUSTER_SLOTS), ['CLUSTER', 'SLOTS'] ); }); diff --git a/packages/client/lib/commands/CLUSTER_SLOTS.ts b/packages/client/lib/commands/CLUSTER_SLOTS.ts index 1b523328bbb..f6f967abe28 100644 --- a/packages/client/lib/commands/CLUSTER_SLOTS.ts +++ b/packages/client/lib/commands/CLUSTER_SLOTS.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { TuplesReply, BlobStringReply, NumberReply, ArrayReply, UnwrapReply, Command } from '../RESP/types'; type RawNode = TuplesReply<[ @@ -16,10 +17,10 @@ type ClusterSlotsRawReply = ArrayReply<[ export type ClusterSlotsNode = ReturnType; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CLUSTER', 'SLOTS']; + parseCommand(parser: CommandParser) { + parser.push('CLUSTER', 'SLOTS'); }, transformReply(reply: UnwrapReply) { return reply.map(([from, to, master, ...replicas]) => ({ diff --git a/packages/client/lib/commands/COMMAND.ts b/packages/client/lib/commands/COMMAND.ts index d9a960107a2..52eb7eb2fea 100644 --- a/packages/client/lib/commands/COMMAND.ts +++ b/packages/client/lib/commands/COMMAND.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, Command, UnwrapReply } from '../RESP/types'; import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers'; export default { + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['COMMAND']; + parseCommand(parser: CommandParser) { + parser.push('COMMAND'); }, // TODO: This works, as we don't currently handle any of the items returned as a map transformReply(reply: UnwrapReply>): Array { diff --git a/packages/client/lib/commands/COMMAND_COUNT.spec.ts b/packages/client/lib/commands/COMMAND_COUNT.spec.ts index 05bd29f223c..a36091df482 100644 --- a/packages/client/lib/commands/COMMAND_COUNT.spec.ts +++ b/packages/client/lib/commands/COMMAND_COUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import COMMAND_COUNT from './COMMAND_COUNT'; +import { parseArgs } from './generic-transformers'; describe('COMMAND COUNT', () => { it('transformArguments', () => { assert.deepEqual( - COMMAND_COUNT.transformArguments(), + parseArgs(COMMAND_COUNT), ['COMMAND', 'COUNT'] ); }); diff --git a/packages/client/lib/commands/COMMAND_COUNT.ts b/packages/client/lib/commands/COMMAND_COUNT.ts index 10b0fdefe09..ef561920b0b 100644 --- a/packages/client/lib/commands/COMMAND_COUNT.ts +++ b/packages/client/lib/commands/COMMAND_COUNT.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['COMMAND', 'COUNT']; + parseCommand(parser: CommandParser) { + parser.push('COMMAND', 'COUNT'); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts b/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts index d5b9f60790d..332e2d51fbd 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import COMMAND_GETKEYS from './COMMAND_GETKEYS'; +import { parseArgs } from './generic-transformers'; describe('COMMAND GETKEYS', () => { it('transformArguments', () => { assert.deepEqual( - COMMAND_GETKEYS.transformArguments(['GET', 'key']), + parseArgs(COMMAND_GETKEYS, ['GET', 'key']), ['COMMAND', 'GETKEYS', 'GET', 'key'] ); }); diff --git a/packages/client/lib/commands/COMMAND_GETKEYS.ts b/packages/client/lib/commands/COMMAND_GETKEYS.ts index 55cca415b8d..97c5cb69ce2 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYS.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(args: Array) { - return ['COMMAND', 'GETKEYS', ...args]; + parseCommand(parser: CommandParser, args: Array) { + parser.push('COMMAND', 'GETKEYS'); + parser.push(...args); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts index a032190c16e..72c1e16a2d1 100644 --- a/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts +++ b/packages/client/lib/commands/COMMAND_GETKEYSANDFLAGS.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, SetReply, UnwrapReply, Command } from '../RESP/types'; export type CommandGetKeysAndFlagsRawReply = ArrayReply>; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(args: Array) { - return ['COMMAND', 'GETKEYSANDFLAGS', ...args]; + parseCommand(parser: CommandParser, args: Array) { + parser.push('COMMAND', 'GETKEYSANDFLAGS'); + parser.push(...args); }, transformReply(reply: UnwrapReply) { return reply.map(entry => { diff --git a/packages/client/lib/commands/COMMAND_INFO.ts b/packages/client/lib/commands/COMMAND_INFO.ts index 5dbd00e8056..fdf03780652 100644 --- a/packages/client/lib/commands/COMMAND_INFO.ts +++ b/packages/client/lib/commands/COMMAND_INFO.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, Command, UnwrapReply } from '../RESP/types'; import { CommandRawReply, CommandReply, transformCommandReply } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(commands: Array) { - return ['COMMAND', 'INFO', ...commands]; + parseCommand(parser: CommandParser, commands: Array) { + parser.push('COMMAND', 'INFO', ...commands); }, // TODO: This works, as we don't currently handle any of the items returned as a map transformReply(reply: UnwrapReply>): Array { diff --git a/packages/client/lib/commands/COMMAND_LIST.spec.ts b/packages/client/lib/commands/COMMAND_LIST.spec.ts index 28a9a203bc8..d2ee9e66161 100644 --- a/packages/client/lib/commands/COMMAND_LIST.spec.ts +++ b/packages/client/lib/commands/COMMAND_LIST.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import COMMAND_LIST from './COMMAND_LIST'; +import { parseArgs } from './generic-transformers'; describe('COMMAND LIST', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,7 +9,7 @@ describe('COMMAND LIST', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - COMMAND_LIST.transformArguments(), + parseArgs(COMMAND_LIST), ['COMMAND', 'LIST'] ); }); @@ -16,7 +17,7 @@ describe('COMMAND LIST', () => { describe('with FILTERBY', () => { it('MODULE', () => { assert.deepEqual( - COMMAND_LIST.transformArguments({ + parseArgs(COMMAND_LIST, { FILTERBY: { type: 'MODULE', value: 'JSON' @@ -28,7 +29,7 @@ describe('COMMAND LIST', () => { it('ACLCAT', () => { assert.deepEqual( - COMMAND_LIST.transformArguments({ + parseArgs(COMMAND_LIST, { FILTERBY: { type: 'ACLCAT', value: 'admin' @@ -40,7 +41,7 @@ describe('COMMAND LIST', () => { it('PATTERN', () => { assert.deepEqual( - COMMAND_LIST.transformArguments({ + parseArgs(COMMAND_LIST, { FILTERBY: { type: 'PATTERN', value: 'a*' diff --git a/packages/client/lib/commands/COMMAND_LIST.ts b/packages/client/lib/commands/COMMAND_LIST.ts index e73cfdc1a0b..ba518b70eca 100644 --- a/packages/client/lib/commands/COMMAND_LIST.ts +++ b/packages/client/lib/commands/COMMAND_LIST.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export const COMMAND_LIST_FILTER_BY = { @@ -16,20 +17,18 @@ export interface CommandListOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(options?: CommandListOptions) { - const args: Array = ['COMMAND', 'LIST']; + parseCommand(parser: CommandParser, options?: CommandListOptions) { + parser.push('COMMAND', 'LIST'); if (options?.FILTERBY) { - args.push( + parser.push( 'FILTERBY', options.FILTERBY.type, options.FILTERBY.value ); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CONFIG_GET.spec.ts b/packages/client/lib/commands/CONFIG_GET.spec.ts index 94bb2fadcb9..411b2ddf472 100644 --- a/packages/client/lib/commands/CONFIG_GET.spec.ts +++ b/packages/client/lib/commands/CONFIG_GET.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CONFIG_GET from './CONFIG_GET'; +import { parseArgs } from './generic-transformers'; describe('CONFIG GET', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - CONFIG_GET.transformArguments('*'), + parseArgs(CONFIG_GET, '*'), ['CONFIG', 'GET', '*'] ); }); it('Array', () => { assert.deepEqual( - CONFIG_GET.transformArguments(['1', '2']), + parseArgs(CONFIG_GET, ['1', '2']), ['CONFIG', 'GET', '1', '2'] ); }); diff --git a/packages/client/lib/commands/CONFIG_GET.ts b/packages/client/lib/commands/CONFIG_GET.ts index 72fb6e56f5f..54fa997bf61 100644 --- a/packages/client/lib/commands/CONFIG_GET.ts +++ b/packages/client/lib/commands/CONFIG_GET.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { MapReply, BlobStringReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments, transformTuplesReply } from './generic-transformers'; +import { RedisVariadicArgument, transformTuplesReply } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(parameters: RedisVariadicArgument) { - return pushVariadicArguments(['CONFIG', 'GET'], parameters); + parseCommand(parser: CommandParser, parameters: RedisVariadicArgument) { + parser.push('CONFIG', 'GET'); + parser.pushVariadic(parameters); }, transformReply: { 2: transformTuplesReply, diff --git a/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts b/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts index c0699e182fc..f2f573df0dc 100644 --- a/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts +++ b/packages/client/lib/commands/CONFIG_RESETSTAT.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import CONFIG_RESETSTAT from './CONFIG_RESETSTAT'; +import { parseArgs } from './generic-transformers'; describe('CONFIG RESETSTAT', () => { it('transformArguments', () => { assert.deepEqual( - CONFIG_RESETSTAT.transformArguments(), + parseArgs(CONFIG_RESETSTAT), ['CONFIG', 'RESETSTAT'] ); }); diff --git a/packages/client/lib/commands/CONFIG_RESETSTAT.ts b/packages/client/lib/commands/CONFIG_RESETSTAT.ts index 4d5deb18b47..15de5ba7808 100644 --- a/packages/client/lib/commands/CONFIG_RESETSTAT.ts +++ b/packages/client/lib/commands/CONFIG_RESETSTAT.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CONFIG', 'RESETSTAT']; + parseCommand(parser: CommandParser) { + parser.push('CONFIG', 'RESETSTAT'); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CONFIG_REWRITE.spec.ts b/packages/client/lib/commands/CONFIG_REWRITE.spec.ts index d612ae216bc..bc006e84c80 100644 --- a/packages/client/lib/commands/CONFIG_REWRITE.spec.ts +++ b/packages/client/lib/commands/CONFIG_REWRITE.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import CONFIG_REWRITE from './CONFIG_REWRITE'; +import { parseArgs } from './generic-transformers'; describe('CONFIG REWRITE', () => { it('transformArguments', () => { assert.deepEqual( - CONFIG_REWRITE.transformArguments(), + parseArgs(CONFIG_REWRITE), ['CONFIG', 'REWRITE'] ); }); diff --git a/packages/client/lib/commands/CONFIG_REWRITE.ts b/packages/client/lib/commands/CONFIG_REWRITE.ts index 6fbc4b1fa27..ae6712ffb57 100644 --- a/packages/client/lib/commands/CONFIG_REWRITE.ts +++ b/packages/client/lib/commands/CONFIG_REWRITE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['CONFIG', 'REWRITE']; + parseCommand(parser: CommandParser) { + parser.push('CONFIG', 'REWRITE'); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/CONFIG_SET.spec.ts b/packages/client/lib/commands/CONFIG_SET.spec.ts index 060183f58d1..56bed2ac46a 100644 --- a/packages/client/lib/commands/CONFIG_SET.spec.ts +++ b/packages/client/lib/commands/CONFIG_SET.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CONFIG_SET from './CONFIG_SET'; +import { parseArgs } from './generic-transformers'; describe('CONFIG SET', () => { describe('transformArguments', () => { it('set one parameter (old version)', () => { assert.deepEqual( - CONFIG_SET.transformArguments('parameter', 'value'), + parseArgs(CONFIG_SET, 'parameter', 'value'), ['CONFIG', 'SET', 'parameter', 'value'] ); }); it('set muiltiple parameters', () => { assert.deepEqual( - CONFIG_SET.transformArguments({ + parseArgs(CONFIG_SET, { 1: 'a', 2: 'b', 3: 'c' diff --git a/packages/client/lib/commands/CONFIG_SET.ts b/packages/client/lib/commands/CONFIG_SET.ts index c7072245e22..dd1bbc29ef2 100644 --- a/packages/client/lib/commands/CONFIG_SET.ts +++ b/packages/client/lib/commands/CONFIG_SET.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command, RedisArgument } from '../RESP/types'; type SingleParameter = [parameter: RedisArgument, value: RedisArgument]; @@ -5,22 +6,21 @@ type SingleParameter = [parameter: RedisArgument, value: RedisArgument]; type MultipleParameters = [config: Record]; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, ...[parameterOrConfig, value]: SingleParameter | MultipleParameters ) { - const args: Array = ['CONFIG', 'SET']; + parser.push('CONFIG', 'SET'); if (typeof parameterOrConfig === 'string' || parameterOrConfig instanceof Buffer) { - args.push(parameterOrConfig, value!); + parser.push(parameterOrConfig, value!); } else { for (const [key, value] of Object.entries(parameterOrConfig)) { - args.push(key, value); + parser.push(key, value); } } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/COPY.spec.ts b/packages/client/lib/commands/COPY.spec.ts index c4c26c30dc2..cd0c6ec9fbe 100644 --- a/packages/client/lib/commands/COPY.spec.ts +++ b/packages/client/lib/commands/COPY.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import COPY from './COPY'; +import { parseArgs } from './generic-transformers'; describe('COPY', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,14 +9,14 @@ describe('COPY', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - COPY.transformArguments('source', 'destination'), + parseArgs(COPY, 'source', 'destination'), ['COPY', 'source', 'destination'] ); }); it('with destination DB flag', () => { assert.deepEqual( - COPY.transformArguments('source', 'destination', { + parseArgs(COPY, 'source', 'destination', { DB: 1 }), ['COPY', 'source', 'destination', 'DB', '1'] @@ -24,7 +25,7 @@ describe('COPY', () => { it('with replace flag', () => { assert.deepEqual( - COPY.transformArguments('source', 'destination', { + parseArgs(COPY, 'source', 'destination', { REPLACE: true }), ['COPY', 'source', 'destination', 'REPLACE'] @@ -33,7 +34,7 @@ describe('COPY', () => { it('with both flags', () => { assert.deepEqual( - COPY.transformArguments('source', 'destination', { + parseArgs(COPY, 'source', 'destination', { DB: 1, REPLACE: true }), diff --git a/packages/client/lib/commands/COPY.ts b/packages/client/lib/commands/COPY.ts index a65948cf944..f26a930264c 100644 --- a/packages/client/lib/commands/COPY.ts +++ b/packages/client/lib/commands/COPY.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export interface CopyCommandOptions { @@ -6,20 +7,18 @@ export interface CopyCommandOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(source: RedisArgument, destination: RedisArgument, options?: CopyCommandOptions) { - const args = ['COPY', source, destination]; + parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, options?: CopyCommandOptions) { + parser.push('COPY'); + parser.pushKeys([source, destination]); if (options?.DB) { - args.push('DB', options.DB.toString()); + parser.push('DB', options.DB.toString()); } if (options?.REPLACE) { - args.push('REPLACE'); + parser.push('REPLACE'); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/DBSIZE.spec.ts b/packages/client/lib/commands/DBSIZE.spec.ts index bd668d166e7..5778e30de3e 100644 --- a/packages/client/lib/commands/DBSIZE.spec.ts +++ b/packages/client/lib/commands/DBSIZE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DBSIZE from './DBSIZE'; +import { parseArgs } from './generic-transformers'; describe('DBSIZE', () => { it('transformArguments', () => { assert.deepEqual( - DBSIZE.transformArguments(), + parseArgs(DBSIZE), ['DBSIZE'] ); }); diff --git a/packages/client/lib/commands/DBSIZE.ts b/packages/client/lib/commands/DBSIZE.ts index 54770831ab0..1ba1f060476 100644 --- a/packages/client/lib/commands/DBSIZE.ts +++ b/packages/client/lib/commands/DBSIZE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['DBSIZE']; + parseCommand(parser: CommandParser) { + parser.push('DBSIZE'); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/DECR.spec.ts b/packages/client/lib/commands/DECR.spec.ts index 80d6c8eb55e..69ff5a5391f 100644 --- a/packages/client/lib/commands/DECR.spec.ts +++ b/packages/client/lib/commands/DECR.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DECR from './DECR'; +import { parseArgs } from './generic-transformers'; describe('DECR', () => { it('transformArguments', () => { assert.deepEqual( - DECR.transformArguments('key'), + parseArgs(DECR, 'key'), ['DECR', 'key'] ); }); diff --git a/packages/client/lib/commands/DECR.ts b/packages/client/lib/commands/DECR.ts index 540148b7eed..b9a6200d81b 100644 --- a/packages/client/lib/commands/DECR.ts +++ b/packages/client/lib/commands/DECR.ts @@ -1,9 +1,10 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument) { - return ['DECR', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('DECR'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/DECRBY.spec.ts b/packages/client/lib/commands/DECRBY.spec.ts index fc0c1033187..ae80fd714e0 100644 --- a/packages/client/lib/commands/DECRBY.spec.ts +++ b/packages/client/lib/commands/DECRBY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DECRBY from './DECRBY'; +import { parseArgs } from './generic-transformers'; describe('DECRBY', () => { it('transformArguments', () => { assert.deepEqual( - DECRBY.transformArguments('key', 2), + parseArgs(DECRBY, 'key', 2), ['DECRBY', 'key', '2'] ); }); diff --git a/packages/client/lib/commands/DECRBY.ts b/packages/client/lib/commands/DECRBY.ts index 77d56939dd5..53a8315130d 100644 --- a/packages/client/lib/commands/DECRBY.ts +++ b/packages/client/lib/commands/DECRBY.ts @@ -1,9 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, decrement: number) { - return ['DECRBY', key, decrement.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, decrement: number) { + parser.push('DECRBY'); + parser.pushKey(key); + parser.push(decrement.toString()); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/DEL.spec.ts b/packages/client/lib/commands/DEL.spec.ts index caac8ac13b5..3d0364e7523 100644 --- a/packages/client/lib/commands/DEL.spec.ts +++ b/packages/client/lib/commands/DEL.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DEL from './DEL'; +import { parseArgs } from './generic-transformers'; describe('DEL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - DEL.transformArguments('key'), + parseArgs(DEL, 'key'), ['DEL', 'key'] ); }); it('array', () => { assert.deepEqual( - DEL.transformArguments(['key1', 'key2']), + parseArgs(DEL, ['key1', 'key2']), ['DEL', 'key1', 'key2'] ); }); diff --git a/packages/client/lib/commands/DEL.ts b/packages/client/lib/commands/DEL.ts index f59a5ba2e89..da0803f4d1b 100644 --- a/packages/client/lib/commands/DEL.ts +++ b/packages/client/lib/commands/DEL.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(keys: RedisVariadicArgument) { - return pushVariadicArguments(['DEL'], keys); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { + parser.push('DEL'); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/DISCARD.spec.ts b/packages/client/lib/commands/DISCARD.spec.ts index 76e0abd57af..7aa769fc2ee 100644 --- a/packages/client/lib/commands/DISCARD.spec.ts +++ b/packages/client/lib/commands/DISCARD.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import DISCARD from './DISCARD'; +import { parseArgs } from './generic-transformers'; describe('DISCARD', () => { it('transformArguments', () => { assert.deepEqual( - DISCARD.transformArguments(), + parseArgs(DISCARD), ['DISCARD'] ); }); diff --git a/packages/client/lib/commands/DISCARD.ts b/packages/client/lib/commands/DISCARD.ts index e153070c989..1d30191c13d 100644 --- a/packages/client/lib/commands/DISCARD.ts +++ b/packages/client/lib/commands/DISCARD.ts @@ -1,8 +1,9 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - transformArguments() { - return ['DISCARD']; + parseCommand(parser: CommandParser) { + parser.push('DISCARD'); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/DUMP.spec.ts b/packages/client/lib/commands/DUMP.spec.ts index 15be3fae086..76fb2ec7c18 100644 --- a/packages/client/lib/commands/DUMP.spec.ts +++ b/packages/client/lib/commands/DUMP.spec.ts @@ -1,7 +1,16 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import DUMP from './DUMP'; +import { parseArgs } from './generic-transformers'; describe('DUMP', () => { + it('transformArguments', () => { + assert.deepEqual( + parseArgs(DUMP, 'key'), + ['DUMP', 'key'] + ); + }); + testUtils.testAll('client.dump', async client => { assert.equal( await client.dump('key'), diff --git a/packages/client/lib/commands/DUMP.ts b/packages/client/lib/commands/DUMP.ts index 06b12f06d09..e442c1cdb2f 100644 --- a/packages/client/lib/commands/DUMP.ts +++ b/packages/client/lib/commands/DUMP.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['DUMP', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('DUMP'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ECHO.spec.ts b/packages/client/lib/commands/ECHO.spec.ts index c0d0725282c..38fd1d4270e 100644 --- a/packages/client/lib/commands/ECHO.spec.ts +++ b/packages/client/lib/commands/ECHO.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ECHO from './ECHO'; +import { parseArgs } from './generic-transformers'; describe('ECHO', () => { it('transformArguments', () => { assert.deepEqual( - ECHO.transformArguments('message'), + parseArgs(ECHO, 'message'), ['ECHO', 'message'] ); }); diff --git a/packages/client/lib/commands/ECHO.ts b/packages/client/lib/commands/ECHO.ts index dfe5ec13000..7935bdc0101 100644 --- a/packages/client/lib/commands/ECHO.ts +++ b/packages/client/lib/commands/ECHO.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(message: RedisArgument) { - return ['ECHO', message]; + parseCommand(parser: CommandParser, message: RedisArgument) { + parser.push('ECHO', message); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/EVAL.spec.ts b/packages/client/lib/commands/EVAL.spec.ts index 2aea64e0991..8ef16eed835 100644 --- a/packages/client/lib/commands/EVAL.spec.ts +++ b/packages/client/lib/commands/EVAL.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import EVAL from './EVAL'; +import { parseArgs } from './generic-transformers'; describe('EVAL', () => { it('transformArguments', () => { assert.deepEqual( - EVAL.transformArguments('return KEYS[1] + ARGV[1]', { + parseArgs(EVAL, 'return KEYS[1] + ARGV[1]', { keys: ['key'], arguments: ['argument'] }), diff --git a/packages/client/lib/commands/EVAL.ts b/packages/client/lib/commands/EVAL.ts index 21684e7a313..cdb8025b0be 100644 --- a/packages/client/lib/commands/EVAL.ts +++ b/packages/client/lib/commands/EVAL.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ReplyUnion, Command } from '../RESP/types'; export interface EvalOptions { @@ -5,29 +6,28 @@ export interface EvalOptions { arguments?: Array; } -export function transformEvalArguments( - command: RedisArgument, +export function parseEvalArguments( + parser: CommandParser, script: RedisArgument, options?: EvalOptions ) { - const args = [command, script]; - + parser.push(script); if (options?.keys) { - args.push(options.keys.length.toString(), ...options.keys); + parser.pushKeysLength(options.keys); } else { - args.push('0'); + parser.push('0'); } if (options?.arguments) { - args.push(...options.arguments); + parser.push(...options.arguments) } - - return args; } export default { - FIRST_KEY_INDEX: (_, options?: EvalOptions) => options?.keys?.[0], IS_READ_ONLY: false, - transformArguments: transformEvalArguments.bind(undefined, 'EVAL'), + parseCommand(...args: Parameters) { + args[0].push('EVAL'); + parseEvalArguments(...args); + }, transformReply: undefined as unknown as () => ReplyUnion } as const satisfies Command; diff --git a/packages/client/lib/commands/EVALSHA.spec.ts b/packages/client/lib/commands/EVALSHA.spec.ts index 81d3a0ec2b0..c491d6e2308 100644 --- a/packages/client/lib/commands/EVALSHA.spec.ts +++ b/packages/client/lib/commands/EVALSHA.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import EVALSHA from './EVALSHA'; +import { parseArgs } from './generic-transformers'; describe('EVALSHA', () => { it('transformArguments', () => { assert.deepEqual( - EVALSHA.transformArguments('sha1', { + parseArgs(EVALSHA, 'sha1', { keys: ['key'], arguments: ['argument'] }), diff --git a/packages/client/lib/commands/EVALSHA.ts b/packages/client/lib/commands/EVALSHA.ts index dc4127f90da..5a9cc771358 100644 --- a/packages/client/lib/commands/EVALSHA.ts +++ b/packages/client/lib/commands/EVALSHA.ts @@ -1,9 +1,11 @@ import { Command } from '../RESP/types'; -import EVAL, { transformEvalArguments } from './EVAL'; +import EVAL, { parseEvalArguments } from './EVAL'; export default { - FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, IS_READ_ONLY: false, - transformArguments: transformEvalArguments.bind(undefined, 'EVALSHA'), + parseCommand(...args: Parameters) { + args[0].push('EVALSHA'); + parseEvalArguments(...args); + }, transformReply: EVAL.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/EVALSHA_RO.spec.ts b/packages/client/lib/commands/EVALSHA_RO.spec.ts index 20b4a27e0d1..d3debe933fe 100644 --- a/packages/client/lib/commands/EVALSHA_RO.spec.ts +++ b/packages/client/lib/commands/EVALSHA_RO.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; import EVALSHA_RO from './EVALSHA_RO'; +import { parseArgs } from './generic-transformers'; describe('EVALSHA_RO', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - EVALSHA_RO.transformArguments('sha1', { + parseArgs(EVALSHA_RO, 'sha1', { keys: ['key'], arguments: ['argument'] }), diff --git a/packages/client/lib/commands/EVALSHA_RO.ts b/packages/client/lib/commands/EVALSHA_RO.ts index fe9042bd5fd..24fadb3f486 100644 --- a/packages/client/lib/commands/EVALSHA_RO.ts +++ b/packages/client/lib/commands/EVALSHA_RO.ts @@ -1,9 +1,11 @@ import { Command } from '../RESP/types'; -import EVAL, { transformEvalArguments } from './EVAL'; +import EVAL, { parseEvalArguments } from './EVAL'; export default { - FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, IS_READ_ONLY: true, - transformArguments: transformEvalArguments.bind(undefined, 'EVALSHA_RO'), + parseCommand(...args: Parameters) { + args[0].push('EVALSHA_RO'); + parseEvalArguments(...args); + }, transformReply: EVAL.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/EVAL_RO.spec.ts b/packages/client/lib/commands/EVAL_RO.spec.ts index 3f071e80681..b5cf1e4e926 100644 --- a/packages/client/lib/commands/EVAL_RO.spec.ts +++ b/packages/client/lib/commands/EVAL_RO.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import EVAL_RO from './EVAL_RO'; +import { parseArgs } from './generic-transformers'; describe('EVAL_RO', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - EVAL_RO.transformArguments('return KEYS[1] + ARGV[1]', { + parseArgs(EVAL_RO, 'return KEYS[1] + ARGV[1]', { keys: ['key'], arguments: ['argument'] }), diff --git a/packages/client/lib/commands/EVAL_RO.ts b/packages/client/lib/commands/EVAL_RO.ts index 2608e28f789..2438fd9d1dd 100644 --- a/packages/client/lib/commands/EVAL_RO.ts +++ b/packages/client/lib/commands/EVAL_RO.ts @@ -1,9 +1,11 @@ import { Command } from '../RESP/types'; -import EVAL, { transformEvalArguments } from './EVAL'; +import EVAL, { parseEvalArguments } from './EVAL'; export default { - FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, IS_READ_ONLY: true, - transformArguments: transformEvalArguments.bind(undefined, 'EVAL_RO'), + parseCommand(...args: Parameters) { + args[0].push('EVAL_RO'); + parseEvalArguments(...args); + }, transformReply: EVAL.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/EXISTS.spec.ts b/packages/client/lib/commands/EXISTS.spec.ts index 695795697f1..d2802dd49b3 100644 --- a/packages/client/lib/commands/EXISTS.spec.ts +++ b/packages/client/lib/commands/EXISTS.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import EXISTS from './EXISTS'; +import { parseArgs } from './generic-transformers'; describe('EXISTS', () => { - describe('transformArguments', () => { + describe('parseCommand', () => { it('string', () => { assert.deepEqual( - EXISTS.transformArguments('key'), + parseArgs(EXISTS, 'key'), ['EXISTS', 'key'] ); }); it('array', () => { assert.deepEqual( - EXISTS.transformArguments(['1', '2']), + parseArgs(EXISTS, ['1', '2']), ['EXISTS', '1', '2'] ); }); diff --git a/packages/client/lib/commands/EXISTS.ts b/packages/client/lib/commands/EXISTS.ts index a077943b8d2..8ebb28269fe 100644 --- a/packages/client/lib/commands/EXISTS.ts +++ b/packages/client/lib/commands/EXISTS.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(keys: RedisVariadicArgument) { - return pushVariadicArguments(['EXISTS'], keys); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { + parser.push('EXISTS'); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/EXPIRE.spec.ts b/packages/client/lib/commands/EXPIRE.spec.ts index 817e37cca45..f3d197b5c69 100644 --- a/packages/client/lib/commands/EXPIRE.spec.ts +++ b/packages/client/lib/commands/EXPIRE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import EXPIRE from './EXPIRE'; +import { parseArgs } from './generic-transformers'; describe('EXPIRE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - EXPIRE.transformArguments('key', 1), + parseArgs(EXPIRE, 'key', 1), ['EXPIRE', 'key', '1'] ); }); it('with set option', () => { assert.deepEqual( - EXPIRE.transformArguments('key', 1, 'NX'), + parseArgs(EXPIRE, 'key', 1, 'NX'), ['EXPIRE', 'key', '1', 'NX'] ); }); diff --git a/packages/client/lib/commands/EXPIRE.ts b/packages/client/lib/commands/EXPIRE.ts index 3e57769bd6e..1e73b629492 100644 --- a/packages/client/lib/commands/EXPIRE.ts +++ b/packages/client/lib/commands/EXPIRE.ts @@ -1,20 +1,20 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, seconds: number, mode?: 'NX' | 'XX' | 'GT' | 'LT' ) { - const args = ['EXPIRE', key, seconds.toString()]; - + parser.push('EXPIRE'); + parser.pushKey(key); + parser.push(seconds.toString()); if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/EXPIREAT.spec.ts b/packages/client/lib/commands/EXPIREAT.spec.ts index 31efdcea398..1949fb051bb 100644 --- a/packages/client/lib/commands/EXPIREAT.spec.ts +++ b/packages/client/lib/commands/EXPIREAT.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import EXPIREAT from './EXPIREAT'; +import { parseArgs } from './generic-transformers'; describe('EXPIREAT', () => { describe('transformArguments', () => { it('number', () => { assert.deepEqual( - EXPIREAT.transformArguments('key', 1), + parseArgs(EXPIREAT, 'key', 1), ['EXPIREAT', 'key', '1'] ); }); @@ -14,14 +15,14 @@ describe('EXPIREAT', () => { it('date', () => { const d = new Date(); assert.deepEqual( - EXPIREAT.transformArguments('key', d), + parseArgs(EXPIREAT, 'key', d), ['EXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString()] ); }); it('with set option', () => { assert.deepEqual( - EXPIREAT.transformArguments('key', 1, 'GT'), + parseArgs(EXPIREAT, 'key', 1, 'GT'), ['EXPIREAT', 'key', '1', 'GT'] ); }); diff --git a/packages/client/lib/commands/EXPIREAT.ts b/packages/client/lib/commands/EXPIREAT.ts index 9a959a87f99..5bcb94ea321 100644 --- a/packages/client/lib/commands/EXPIREAT.ts +++ b/packages/client/lib/commands/EXPIREAT.ts @@ -1,21 +1,21 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformEXAT } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, timestamp: number | Date, mode?: 'NX' | 'XX' | 'GT' | 'LT' ) { - const args = ['EXPIREAT', key, transformEXAT(timestamp)]; - + parser.push('EXPIREAT'); + parser.pushKey(key); + parser.push(transformEXAT(timestamp)); if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/EXPIRETIME.spec.ts b/packages/client/lib/commands/EXPIRETIME.spec.ts index 3c202d2427f..f2c8d3d4521 100644 --- a/packages/client/lib/commands/EXPIRETIME.spec.ts +++ b/packages/client/lib/commands/EXPIRETIME.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import EXPIRETIME from './EXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('EXPIRETIME', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - EXPIRETIME.transformArguments('key'), + parseArgs(EXPIRETIME, 'key'), ['EXPIRETIME', 'key'] ); }); diff --git a/packages/client/lib/commands/EXPIRETIME.ts b/packages/client/lib/commands/EXPIRETIME.ts index d6ac35aeb3d..2bb97fb737b 100644 --- a/packages/client/lib/commands/EXPIRETIME.ts +++ b/packages/client/lib/commands/EXPIRETIME.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['EXPIRETIME', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('EXPIRETIME'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FAILOVER.spec.ts b/packages/client/lib/commands/FAILOVER.spec.ts index 96602caff91..b23c3516f03 100644 --- a/packages/client/lib/commands/FAILOVER.spec.ts +++ b/packages/client/lib/commands/FAILOVER.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import FAILOVER from './FAILOVER'; +import { parseArgs } from './generic-transformers'; describe('FAILOVER', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - FAILOVER.transformArguments(), + parseArgs(FAILOVER), ['FAILOVER'] ); }); @@ -13,7 +14,7 @@ describe('FAILOVER', () => { describe('with TO', () => { it('simple', () => { assert.deepEqual( - FAILOVER.transformArguments({ + parseArgs(FAILOVER, { TO: { host: 'host', port: 6379 @@ -25,7 +26,7 @@ describe('FAILOVER', () => { it('with FORCE', () => { assert.deepEqual( - FAILOVER.transformArguments({ + parseArgs(FAILOVER, { TO: { host: 'host', port: 6379, @@ -39,7 +40,7 @@ describe('FAILOVER', () => { it('with ABORT', () => { assert.deepEqual( - FAILOVER.transformArguments({ + parseArgs(FAILOVER, { ABORT: true }), ['FAILOVER', 'ABORT'] @@ -48,7 +49,7 @@ describe('FAILOVER', () => { it('with TIMEOUT', () => { assert.deepEqual( - FAILOVER.transformArguments({ + parseArgs(FAILOVER, { TIMEOUT: 1 }), ['FAILOVER', 'TIMEOUT', '1'] @@ -57,7 +58,7 @@ describe('FAILOVER', () => { it('with TO, ABORT, TIMEOUT', () => { assert.deepEqual( - FAILOVER.transformArguments({ + parseArgs(FAILOVER, { TO: { host: 'host', port: 6379 diff --git a/packages/client/lib/commands/FAILOVER.ts b/packages/client/lib/commands/FAILOVER.ts index 0c1e710d321..1e98b983f96 100644 --- a/packages/client/lib/commands/FAILOVER.ts +++ b/packages/client/lib/commands/FAILOVER.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; interface FailoverOptions { @@ -11,26 +12,24 @@ interface FailoverOptions { } export default { - transformArguments(options?: FailoverOptions) { - const args = ['FAILOVER']; + parseCommand(parser: CommandParser, options?: FailoverOptions) { + parser.push('FAILOVER'); if (options?.TO) { - args.push('TO', options.TO.host, options.TO.port.toString()); + parser.push('TO', options.TO.host, options.TO.port.toString()); if (options.TO.FORCE) { - args.push('FORCE'); + parser.push('FORCE'); } } if (options?.ABORT) { - args.push('ABORT'); + parser.push('ABORT'); } if (options?.TIMEOUT) { - args.push('TIMEOUT', options.TIMEOUT.toString()); + parser.push('TIMEOUT', options.TIMEOUT.toString()); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FCALL.spec.ts b/packages/client/lib/commands/FCALL.spec.ts index 286f2a371bf..6c3a65c1448 100644 --- a/packages/client/lib/commands/FCALL.spec.ts +++ b/packages/client/lib/commands/FCALL.spec.ts @@ -2,13 +2,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; import FCALL from './FCALL'; +import { parseArgs } from './generic-transformers'; describe('FCALL', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - FCALL.transformArguments('function', { + parseArgs(FCALL, 'function', { keys: ['key'], arguments: ['argument'] }), diff --git a/packages/client/lib/commands/FCALL.ts b/packages/client/lib/commands/FCALL.ts index ba371a81b13..622060f693c 100644 --- a/packages/client/lib/commands/FCALL.ts +++ b/packages/client/lib/commands/FCALL.ts @@ -1,9 +1,11 @@ import { Command } from '../RESP/types'; -import EVAL, { transformEvalArguments } from './EVAL'; +import EVAL, { parseEvalArguments } from './EVAL'; export default { - FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, IS_READ_ONLY: false, - transformArguments: transformEvalArguments.bind(undefined, 'FCALL'), + parseCommand(...args: Parameters) { + args[0].push('FCALL'); + parseEvalArguments(...args); + }, transformReply: EVAL.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FCALL_RO.spec.ts b/packages/client/lib/commands/FCALL_RO.spec.ts index 57edcebebef..447e00072be 100644 --- a/packages/client/lib/commands/FCALL_RO.spec.ts +++ b/packages/client/lib/commands/FCALL_RO.spec.ts @@ -2,13 +2,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; import FCALL_RO from './FCALL_RO'; +import { parseArgs } from './generic-transformers'; describe('FCALL_RO', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - FCALL_RO.transformArguments('function', { + parseArgs(FCALL_RO, 'function', { keys: ['key'], arguments: ['argument'] }), diff --git a/packages/client/lib/commands/FCALL_RO.ts b/packages/client/lib/commands/FCALL_RO.ts index ec002a79f82..95effb0e698 100644 --- a/packages/client/lib/commands/FCALL_RO.ts +++ b/packages/client/lib/commands/FCALL_RO.ts @@ -1,9 +1,11 @@ import { Command } from '../RESP/types'; -import EVAL, { transformEvalArguments } from './EVAL'; +import EVAL, { parseEvalArguments } from './EVAL'; export default { - FIRST_KEY_INDEX: EVAL.FIRST_KEY_INDEX, IS_READ_ONLY: false, - transformArguments: transformEvalArguments.bind(undefined, 'FCALL_RO'), + parseCommand(...args: Parameters) { + args[0].push('FCALL_RO'); + parseEvalArguments(...args); + }, transformReply: EVAL.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FLUSHALL.spec.ts b/packages/client/lib/commands/FLUSHALL.spec.ts index 63ad38dd7de..86daff1973a 100644 --- a/packages/client/lib/commands/FLUSHALL.spec.ts +++ b/packages/client/lib/commands/FLUSHALL.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FLUSHALL, { REDIS_FLUSH_MODES } from './FLUSHALL'; +import { parseArgs } from './generic-transformers'; describe('FLUSHALL', () => { describe('transformArguments', () => { it('default', () => { assert.deepEqual( - FLUSHALL.transformArguments(), + parseArgs(FLUSHALL), ['FLUSHALL'] ); }); it('ASYNC', () => { assert.deepEqual( - FLUSHALL.transformArguments(REDIS_FLUSH_MODES.ASYNC), + parseArgs(FLUSHALL,REDIS_FLUSH_MODES.ASYNC), ['FLUSHALL', 'ASYNC'] ); }); it('SYNC', () => { assert.deepEqual( - FLUSHALL.transformArguments(REDIS_FLUSH_MODES.SYNC), + parseArgs(FLUSHALL, REDIS_FLUSH_MODES.SYNC), ['FLUSHALL', 'SYNC'] ); }); diff --git a/packages/client/lib/commands/FLUSHALL.ts b/packages/client/lib/commands/FLUSHALL.ts index 5e6484a991e..c39535e8864 100644 --- a/packages/client/lib/commands/FLUSHALL.ts +++ b/packages/client/lib/commands/FLUSHALL.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export const REDIS_FLUSH_MODES = { @@ -8,16 +9,13 @@ export const REDIS_FLUSH_MODES = { export type RedisFlushMode = typeof REDIS_FLUSH_MODES[keyof typeof REDIS_FLUSH_MODES]; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(mode?: RedisFlushMode) { - const args = ['FLUSHALL']; - + parseCommand(parser: CommandParser, mode?: RedisFlushMode) { + parser.push('FLUSHALL'); if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FLUSHDB.spec.ts b/packages/client/lib/commands/FLUSHDB.spec.ts index ad09ecfc945..795df637cb4 100644 --- a/packages/client/lib/commands/FLUSHDB.spec.ts +++ b/packages/client/lib/commands/FLUSHDB.spec.ts @@ -2,26 +2,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FLUSHDB from './FLUSHDB'; import { REDIS_FLUSH_MODES } from './FLUSHALL'; +import { parseArgs } from './generic-transformers'; describe('FLUSHDB', () => { describe('transformArguments', () => { it('default', () => { assert.deepEqual( - FLUSHDB.transformArguments(), + parseArgs(FLUSHDB), ['FLUSHDB'] ); }); it('ASYNC', () => { assert.deepEqual( - FLUSHDB.transformArguments(REDIS_FLUSH_MODES.ASYNC), + parseArgs(FLUSHDB, REDIS_FLUSH_MODES.ASYNC), ['FLUSHDB', 'ASYNC'] ); }); it('SYNC', () => { assert.deepEqual( - FLUSHDB.transformArguments(REDIS_FLUSH_MODES.SYNC), + parseArgs(FLUSHDB, REDIS_FLUSH_MODES.SYNC), ['FLUSHDB', 'SYNC'] ); }); diff --git a/packages/client/lib/commands/FLUSHDB.ts b/packages/client/lib/commands/FLUSHDB.ts index 75c7a66f190..5639f69a611 100644 --- a/packages/client/lib/commands/FLUSHDB.ts +++ b/packages/client/lib/commands/FLUSHDB.ts @@ -1,17 +1,15 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; import { RedisFlushMode } from './FLUSHALL'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(mode?: RedisFlushMode) { - const args = ['FLUSHDB']; - + parseCommand(parser: CommandParser, mode?: RedisFlushMode) { + parser.push('FLUSHDB'); if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_DELETE.spec.ts b/packages/client/lib/commands/FUNCTION_DELETE.spec.ts index 1172e84b956..b33ea25916b 100644 --- a/packages/client/lib/commands/FUNCTION_DELETE.spec.ts +++ b/packages/client/lib/commands/FUNCTION_DELETE.spec.ts @@ -2,13 +2,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FUNCTION_DELETE from './FUNCTION_DELETE'; import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION DELETE', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - FUNCTION_DELETE.transformArguments('library'), + parseArgs(FUNCTION_DELETE, 'library'), ['FUNCTION', 'DELETE', 'library'] ); }); diff --git a/packages/client/lib/commands/FUNCTION_DELETE.ts b/packages/client/lib/commands/FUNCTION_DELETE.ts index 1061cded17c..dbfb044928e 100644 --- a/packages/client/lib/commands/FUNCTION_DELETE.ts +++ b/packages/client/lib/commands/FUNCTION_DELETE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(library: RedisArgument) { - return ['FUNCTION', 'DELETE', library]; + parseCommand(parser: CommandParser, library: RedisArgument) { + parser.push('FUNCTION', 'DELETE', library); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_DUMP.spec.ts b/packages/client/lib/commands/FUNCTION_DUMP.spec.ts index 4d4e885e4f2..bbd6302bb6a 100644 --- a/packages/client/lib/commands/FUNCTION_DUMP.spec.ts +++ b/packages/client/lib/commands/FUNCTION_DUMP.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FUNCTION_DUMP from './FUNCTION_DUMP'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION DUMP', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - FUNCTION_DUMP.transformArguments(), + parseArgs(FUNCTION_DUMP), ['FUNCTION', 'DUMP'] ); }); diff --git a/packages/client/lib/commands/FUNCTION_DUMP.ts b/packages/client/lib/commands/FUNCTION_DUMP.ts index 8f6ff047fa7..2d0dbdd4455 100644 --- a/packages/client/lib/commands/FUNCTION_DUMP.ts +++ b/packages/client/lib/commands/FUNCTION_DUMP.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['FUNCTION', 'DUMP']; + parseCommand(parser: CommandParser) { + parser.push('FUNCTION', 'DUMP') }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts b/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts index 5601784ed6a..4fe90bdb607 100644 --- a/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts +++ b/packages/client/lib/commands/FUNCTION_FLUSH.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FUNCTION_FLUSH from './FUNCTION_FLUSH'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION FLUSH', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,14 +9,14 @@ describe('FUNCTION FLUSH', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - FUNCTION_FLUSH.transformArguments(), + parseArgs(FUNCTION_FLUSH), ['FUNCTION', 'FLUSH'] ); }); it('with mode', () => { assert.deepEqual( - FUNCTION_FLUSH.transformArguments('SYNC'), + parseArgs(FUNCTION_FLUSH, 'SYNC'), ['FUNCTION', 'FLUSH', 'SYNC'] ); }); diff --git a/packages/client/lib/commands/FUNCTION_FLUSH.ts b/packages/client/lib/commands/FUNCTION_FLUSH.ts index 844d3586d90..4ca59e4464e 100644 --- a/packages/client/lib/commands/FUNCTION_FLUSH.ts +++ b/packages/client/lib/commands/FUNCTION_FLUSH.ts @@ -1,17 +1,16 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; import { RedisFlushMode } from './FLUSHALL'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(mode?: RedisFlushMode) { - const args = ['FUNCTION', 'FLUSH']; - + parseCommand(parser: CommandParser, mode?: RedisFlushMode) { + parser.push('FUNCTION', 'FLUSH'); + if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_KILL.spec.ts b/packages/client/lib/commands/FUNCTION_KILL.spec.ts index be231e41180..c4dbd124d30 100644 --- a/packages/client/lib/commands/FUNCTION_KILL.spec.ts +++ b/packages/client/lib/commands/FUNCTION_KILL.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils from '../test-utils'; import FUNCTION_KILL from './FUNCTION_KILL'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION KILL', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - FUNCTION_KILL.transformArguments(), + parseArgs(FUNCTION_KILL), ['FUNCTION', 'KILL'] ); }); diff --git a/packages/client/lib/commands/FUNCTION_KILL.ts b/packages/client/lib/commands/FUNCTION_KILL.ts index f452b0b80d9..8b5351e93ab 100644 --- a/packages/client/lib/commands/FUNCTION_KILL.ts +++ b/packages/client/lib/commands/FUNCTION_KILL.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['FUNCTION', 'KILL']; + parseCommand(parser: CommandParser) { + parser.push('FUNCTION', 'KILL'); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_LIST.spec.ts b/packages/client/lib/commands/FUNCTION_LIST.spec.ts index e269d3150b6..6d9b28acf90 100644 --- a/packages/client/lib/commands/FUNCTION_LIST.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LIST.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FUNCTION_LIST from './FUNCTION_LIST'; import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION LIST', () => { testUtils.isVersionGreaterThanHook([7]); @@ -9,14 +10,14 @@ describe('FUNCTION LIST', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - FUNCTION_LIST.transformArguments(), + parseArgs(FUNCTION_LIST), ['FUNCTION', 'LIST'] ); }); it('with LIBRARYNAME', () => { assert.deepEqual( - FUNCTION_LIST.transformArguments({ + parseArgs(FUNCTION_LIST, { LIBRARYNAME: 'patter*' }), ['FUNCTION', 'LIST', 'LIBRARYNAME', 'patter*'] diff --git a/packages/client/lib/commands/FUNCTION_LIST.ts b/packages/client/lib/commands/FUNCTION_LIST.ts index 07c1ff2a000..82e3697eadc 100644 --- a/packages/client/lib/commands/FUNCTION_LIST.ts +++ b/packages/client/lib/commands/FUNCTION_LIST.ts @@ -1,4 +1,5 @@ -import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, NullReply, SetReply, UnwrapReply, Resp2Reply, CommandArguments, Command } from '../RESP/types'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, NullReply, SetReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; export interface FunctionListOptions { LIBRARYNAME?: RedisArgument; @@ -17,16 +18,14 @@ export type FunctionListReplyItem = [ export type FunctionListReply = ArrayReply>; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(options?: FunctionListOptions) { - const args: CommandArguments = ['FUNCTION', 'LIST']; + parseCommand(parser: CommandParser, options?: FunctionListOptions) { + parser.push('FUNCTION', 'LIST'); if (options?.LIBRARYNAME) { - args.push('LIBRARYNAME', options.LIBRARYNAME); + parser.push('LIBRARYNAME', options.LIBRARYNAME); } - - return args; }, transformReply: { 2: (reply: UnwrapReply>) => { diff --git a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts index 8ff40582460..f44db9ba037 100644 --- a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FUNCTION_LIST_WITHCODE from './FUNCTION_LIST_WITHCODE'; import { MATH_FUNCTION, loadMathFunction } from './FUNCTION_LOAD.spec'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION LIST WITHCODE', () => { testUtils.isVersionGreaterThanHook([7]); @@ -9,14 +10,14 @@ describe('FUNCTION LIST WITHCODE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - FUNCTION_LIST_WITHCODE.transformArguments(), + parseArgs(FUNCTION_LIST_WITHCODE), ['FUNCTION', 'LIST', 'WITHCODE'] ); }); it('with LIBRARYNAME', () => { assert.deepEqual( - FUNCTION_LIST_WITHCODE.transformArguments({ + parseArgs(FUNCTION_LIST_WITHCODE, { LIBRARYNAME: 'patter*' }), ['FUNCTION', 'LIST', 'LIBRARYNAME', 'patter*', 'WITHCODE'] diff --git a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts index 47a02a3da8a..208bc5fd303 100644 --- a/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts +++ b/packages/client/lib/commands/FUNCTION_LIST_WITHCODE.ts @@ -7,12 +7,11 @@ export type FunctionListWithCodeReply = ArrayReply>; export default { - FIRST_KEY_INDEX: FUNCTION_LIST.FIRST_KEY_INDEX, + NOT_KEYED_COMMAND: FUNCTION_LIST.NOT_KEYED_COMMAND, IS_READ_ONLY: FUNCTION_LIST.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = FUNCTION_LIST.transformArguments(...args); - redisArgs.push('WITHCODE'); - return redisArgs; + parseCommand(...args: Parameters) { + FUNCTION_LIST.parseCommand(...args); + args[0].push('WITHCODE'); }, transformReply: { 2: (reply: UnwrapReply>) => { diff --git a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts index fe896bdf8c8..c0a511bffc9 100644 --- a/packages/client/lib/commands/FUNCTION_LOAD.spec.ts +++ b/packages/client/lib/commands/FUNCTION_LOAD.spec.ts @@ -3,6 +3,8 @@ import testUtils, { GLOBAL } from '../test-utils'; import FUNCTION_LOAD from './FUNCTION_LOAD'; import { RedisClientType } from '../client'; import { NumberReply, RedisFunctions, RedisModules, RedisScripts, RespVersions } from '../RESP/types'; +import { parseArgs } from './generic-transformers'; +import { CommandParser } from '../client/parser'; @@ -25,8 +27,8 @@ export const MATH_FUNCTION = { IS_READ_ONLY: true, NUMBER_OF_KEYS: 1, FIRST_KEY_INDEX: 0, - transformArguments(key: string) { - return [key]; + parseCommand(parser: CommandParser, key: string) { + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } @@ -53,14 +55,14 @@ describe('FUNCTION LOAD', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - FUNCTION_LOAD.transformArguments('code'), + parseArgs(FUNCTION_LOAD, 'code'), ['FUNCTION', 'LOAD', 'code'] ); }); it('with REPLACE', () => { assert.deepEqual( - FUNCTION_LOAD.transformArguments('code', { + parseArgs(FUNCTION_LOAD, 'code', { REPLACE: true }), ['FUNCTION', 'LOAD', 'REPLACE', 'code'] diff --git a/packages/client/lib/commands/FUNCTION_LOAD.ts b/packages/client/lib/commands/FUNCTION_LOAD.ts index 32b030909ad..40b8ea8c0f4 100644 --- a/packages/client/lib/commands/FUNCTION_LOAD.ts +++ b/packages/client/lib/commands/FUNCTION_LOAD.ts @@ -1,22 +1,21 @@ -import { RedisArgument, CommandArguments, BlobStringReply, Command } from '../RESP/types'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export interface FunctionLoadOptions { REPLACE?: boolean; } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(code: RedisArgument, options?: FunctionLoadOptions) { - const args: CommandArguments = ['FUNCTION', 'LOAD']; + parseCommand(parser: CommandParser, code: RedisArgument, options?: FunctionLoadOptions) { + parser.push('FUNCTION', 'LOAD'); if (options?.REPLACE) { - args.push('REPLACE'); + parser.push('REPLACE'); } - args.push(code); - - return args; + parser.push(code); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts b/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts index 465e99b6104..72d7d1d6204 100644 --- a/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts +++ b/packages/client/lib/commands/FUNCTION_RESTORE.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FUNCTION_RESTORE from './FUNCTION_RESTORE'; import { RESP_TYPES } from '../RESP/decoder'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION RESTORE', () => { testUtils.isVersionGreaterThanHook([7]); @@ -9,14 +10,14 @@ describe('FUNCTION RESTORE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - FUNCTION_RESTORE.transformArguments('dump'), + parseArgs(FUNCTION_RESTORE, 'dump'), ['FUNCTION', 'RESTORE', 'dump'] ); }); it('with mode', () => { assert.deepEqual( - FUNCTION_RESTORE.transformArguments('dump', { + parseArgs(FUNCTION_RESTORE, 'dump', { mode: 'APPEND' }), ['FUNCTION', 'RESTORE', 'dump', 'APPEND'] diff --git a/packages/client/lib/commands/FUNCTION_RESTORE.ts b/packages/client/lib/commands/FUNCTION_RESTORE.ts index 8c875530562..944813f25e5 100644 --- a/packages/client/lib/commands/FUNCTION_RESTORE.ts +++ b/packages/client/lib/commands/FUNCTION_RESTORE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command, RedisArgument } from '../RESP/types'; export interface FunctionRestoreOptions { @@ -5,16 +6,14 @@ export interface FunctionRestoreOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(dump: RedisArgument, options?: FunctionRestoreOptions) { - const args = ['FUNCTION', 'RESTORE', dump]; + parseCommand(parser: CommandParser, dump: RedisArgument, options?: FunctionRestoreOptions) { + parser.push('FUNCTION', 'RESTORE', dump); if (options?.mode) { - args.push(options.mode); + parser.push(options.mode); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/FUNCTION_STATS.spec.ts b/packages/client/lib/commands/FUNCTION_STATS.spec.ts index b48b012614f..a3c5e00fe72 100644 --- a/packages/client/lib/commands/FUNCTION_STATS.spec.ts +++ b/packages/client/lib/commands/FUNCTION_STATS.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FUNCTION_STATS from './FUNCTION_STATS'; +import { parseArgs } from './generic-transformers'; describe('FUNCTION STATS', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - FUNCTION_STATS.transformArguments(), + parseArgs(FUNCTION_STATS), ['FUNCTION', 'STATS'] ); }); diff --git a/packages/client/lib/commands/FUNCTION_STATS.ts b/packages/client/lib/commands/FUNCTION_STATS.ts index 138d1fb82d5..908be5476e0 100644 --- a/packages/client/lib/commands/FUNCTION_STATS.ts +++ b/packages/client/lib/commands/FUNCTION_STATS.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { Command, TuplesToMapReply, BlobStringReply, NullReply, NumberReply, MapReply, Resp2Reply, UnwrapReply } from '../RESP/types'; import { isNullReply } from './generic-transformers'; @@ -20,10 +21,10 @@ type FunctionStatsReply = TuplesToMapReply<[ ]>; export default { + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - FIRST_KEY_INDEX: undefined, - transformArguments() { - return ['FUNCTION', 'STATS']; + parseCommand(parser: CommandParser) { + parser.push('FUNCTION', 'STATS'); }, transformReply: { 2: (reply: UnwrapReply>) => { diff --git a/packages/client/lib/commands/GEOADD.spec.ts b/packages/client/lib/commands/GEOADD.spec.ts index 14195ed289c..d947141a318 100644 --- a/packages/client/lib/commands/GEOADD.spec.ts +++ b/packages/client/lib/commands/GEOADD.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEOADD from './GEOADD'; +import { parseArgs } from './generic-transformers'; describe('GEOADD', () => { describe('transformArguments', () => { it('one member', () => { assert.deepEqual( - GEOADD.transformArguments('key', { + parseArgs(GEOADD, 'key', { member: 'member', longitude: 1, latitude: 2 @@ -17,7 +18,7 @@ describe('GEOADD', () => { it('multiple members', () => { assert.deepEqual( - GEOADD.transformArguments('key', [{ + parseArgs(GEOADD, 'key', [{ longitude: 1, latitude: 2, member: '3', @@ -32,7 +33,7 @@ describe('GEOADD', () => { it('with condition', () => { assert.deepEqual( - GEOADD.transformArguments('key', { + parseArgs(GEOADD, 'key', { longitude: 1, latitude: 2, member: 'member' @@ -45,7 +46,7 @@ describe('GEOADD', () => { it('with NX (backwards compatibility)', () => { assert.deepEqual( - GEOADD.transformArguments('key', { + parseArgs(GEOADD, 'key', { longitude: 1, latitude: 2, member: 'member' @@ -58,7 +59,7 @@ describe('GEOADD', () => { it('with CH', () => { assert.deepEqual( - GEOADD.transformArguments('key', { + parseArgs(GEOADD, 'key', { longitude: 1, latitude: 2, member: 'member' @@ -71,7 +72,7 @@ describe('GEOADD', () => { it('with condition, CH', () => { assert.deepEqual( - GEOADD.transformArguments('key', { + parseArgs(GEOADD, 'key', { longitude: 1, latitude: 2, member: 'member' diff --git a/packages/client/lib/commands/GEOADD.ts b/packages/client/lib/commands/GEOADD.ts index f89f6b80e82..31bf457e158 100644 --- a/packages/client/lib/commands/GEOADD.ts +++ b/packages/client/lib/commands/GEOADD.ts @@ -1,4 +1,5 @@ -import { RedisArgument, CommandArguments, NumberReply, Command } from '../RESP/types'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { GeoCoordinates } from './GEOSEARCH'; export interface GeoMember extends GeoCoordinates { @@ -19,45 +20,45 @@ export interface GeoAddOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, toAdd: GeoMember | Array, options?: GeoAddOptions ) { - const args = ['GEOADD', key]; + parser.push('GEOADD') + parser.pushKey(key); if (options?.condition) { - args.push(options.condition); + parser.push(options.condition); } else if (options?.NX) { - args.push('NX'); + parser.push('NX'); } else if (options?.XX) { - args.push('XX'); + parser.push('XX'); } if (options?.CH) { - args.push('CH'); + parser.push('CH'); } if (Array.isArray(toAdd)) { for (const member of toAdd) { - pushMember(args, member); + pushMember(parser, member); } } else { - pushMember(args, toAdd); + pushMember(parser, toAdd); } - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; function pushMember( - args: CommandArguments, + parser: CommandParser, { longitude, latitude, member }: GeoMember ) { - args.push( + parser.push( longitude.toString(), latitude.toString(), member diff --git a/packages/client/lib/commands/GEODIST.spec.ts b/packages/client/lib/commands/GEODIST.spec.ts index eb5a1ef801e..a23df405d1d 100644 --- a/packages/client/lib/commands/GEODIST.spec.ts +++ b/packages/client/lib/commands/GEODIST.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEODIST from './GEODIST'; +import { parseArgs } from './generic-transformers'; describe('GEODIST', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - GEODIST.transformArguments('key', '1', '2'), + parseArgs(GEODIST, 'key', '1', '2'), ['GEODIST', 'key', '1', '2'] ); }); it('with unit', () => { assert.deepEqual( - GEODIST.transformArguments('key', '1', '2', 'm'), + parseArgs(GEODIST, 'key', '1', '2', 'm'), ['GEODIST', 'key', '1', '2', 'm'] ); }); diff --git a/packages/client/lib/commands/GEODIST.ts b/packages/client/lib/commands/GEODIST.ts index 3e684d67579..ba4d3080a71 100644 --- a/packages/client/lib/commands/GEODIST.ts +++ b/packages/client/lib/commands/GEODIST.ts @@ -1,22 +1,23 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; import { GeoUnits } from './GEOSEARCH'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( + parseCommand(parser: CommandParser, key: RedisArgument, member1: RedisArgument, member2: RedisArgument, unit?: GeoUnits ) { - const args = ['GEODIST', key, member1, member2]; + parser.push('GEODIST'); + parser.pushKey(key); + parser.push(member1, member2); if (unit) { - args.push(unit); + parser.push(unit); } - - return args; }, transformReply(reply: BlobStringReply | NullReply) { return reply === null ? null : Number(reply); diff --git a/packages/client/lib/commands/GEOHASH.spec.ts b/packages/client/lib/commands/GEOHASH.spec.ts index 8efe55d89b6..ad26dff8434 100644 --- a/packages/client/lib/commands/GEOHASH.spec.ts +++ b/packages/client/lib/commands/GEOHASH.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEOHASH from './GEOHASH'; +import { parseArgs } from './generic-transformers'; describe('GEOHASH', () => { describe('transformArguments', () => { it('single member', () => { assert.deepEqual( - GEOHASH.transformArguments('key', 'member'), + parseArgs(GEOHASH, 'key', 'member'), ['GEOHASH', 'key', 'member'] ); }); it('multiple members', () => { assert.deepEqual( - GEOHASH.transformArguments('key', ['1', '2']), + parseArgs(GEOHASH, 'key', ['1', '2']), ['GEOHASH', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/GEOHASH.ts b/packages/client/lib/commands/GEOHASH.ts index d8d2732e512..c3265d13157 100644 --- a/packages/client/lib/commands/GEOHASH.ts +++ b/packages/client/lib/commands/GEOHASH.ts @@ -1,14 +1,14 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( - key: RedisArgument, - member: RedisVariadicArgument - ) { - return pushVariadicArguments(['GEOHASH', key], member); + parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { + parser.push('GEOHASH'); + parser.pushKey(key); + parser.pushVariadic(member); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEOPOS.spec.ts b/packages/client/lib/commands/GEOPOS.spec.ts index 20ad5c5c942..247dd91d222 100644 --- a/packages/client/lib/commands/GEOPOS.spec.ts +++ b/packages/client/lib/commands/GEOPOS.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEOPOS from './GEOPOS'; +import { parseArgs } from './generic-transformers'; describe('GEOPOS', () => { describe('transformArguments', () => { it('single member', () => { assert.deepEqual( - GEOPOS.transformArguments('key', 'member'), + parseArgs(GEOPOS, 'key', 'member'), ['GEOPOS', 'key', 'member'] ); }); it('multiple members', () => { assert.deepEqual( - GEOPOS.transformArguments('key', ['1', '2']), + parseArgs(GEOPOS, 'key', ['1', '2']), ['GEOPOS', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/GEOPOS.ts b/packages/client/lib/commands/GEOPOS.ts index 30273c64c18..6bdbb65ac46 100644 --- a/packages/client/lib/commands/GEOPOS.ts +++ b/packages/client/lib/commands/GEOPOS.ts @@ -1,14 +1,14 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( - key: RedisArgument, - member: RedisVariadicArgument - ) { - return pushVariadicArguments(['GEOPOS', key], member); + parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { + parser.push('GEOPOS'); + parser.pushKey(key); + parser.pushVariadic(member); }, transformReply(reply: UnwrapReply | NullReply>>) { return reply.map(item => { diff --git a/packages/client/lib/commands/GEORADIUS.spec.ts b/packages/client/lib/commands/GEORADIUS.spec.ts index 533e48f6898..3c33395c5f6 100644 --- a/packages/client/lib/commands/GEORADIUS.spec.ts +++ b/packages/client/lib/commands/GEORADIUS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUS from './GEORADIUS'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUS', () => { it('transformArguments', () => { assert.deepEqual( - GEORADIUS.transformArguments('key', { + parseArgs(GEORADIUS, 'key', { longitude: 1, latitude: 2 }, 3, 'm'), diff --git a/packages/client/lib/commands/GEORADIUS.ts b/packages/client/lib/commands/GEORADIUS.ts index e777432f090..5e8d880ab5e 100644 --- a/packages/client/lib/commands/GEORADIUS.ts +++ b/packages/client/lib/commands/GEORADIUS.ts @@ -1,32 +1,27 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { GeoCoordinates, GeoUnits, GeoSearchOptions, pushGeoSearchOptions } from './GEOSEARCH'; +import { GeoCoordinates, GeoUnits, GeoSearchOptions, parseGeoSearchOptions } from './GEOSEARCH'; -export function transformGeoRadiusArguments( - command: RedisArgument, +export function parseGeoRadiusArguments( + parser: CommandParser, key: RedisArgument, from: GeoCoordinates, radius: number, unit: GeoUnits, options?: GeoSearchOptions ) { - const args = [ - command, - key, - from.longitude.toString(), - from.latitude.toString(), - radius.toString(), - unit - ]; + parser.pushKey(key); + parser.push(from.longitude.toString(), from.latitude.toString(), radius.toString(), unit); - pushGeoSearchOptions(args, options); - - return args; + parseGeoSearchOptions(parser, options) } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments: transformGeoRadiusArguments.bind(undefined, 'GEORADIUS'), + parseCommand(...args: Parameters) { + args[0].push('GEORADIUS'); + return parseGeoRadiusArguments(...args); + }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts index 57349a79acb..c81c3d75815 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUSBYMEMBER', () => { it('transformArguments', () => { assert.deepEqual( - GEORADIUSBYMEMBER.transformArguments('key', 'member', 3, 'm'), + parseArgs(GEORADIUSBYMEMBER, 'key', 'member', 3, 'm'), ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm'] ); }); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts index 13b52a30630..be4ca54650c 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER.ts @@ -1,30 +1,33 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { GeoUnits, GeoSearchOptions, pushGeoSearchOptions } from './GEOSEARCH'; +import { GeoUnits, GeoSearchOptions, parseGeoSearchOptions } from './GEOSEARCH'; -export function transformGeoRadiusByMemberArguments( - command: RedisArgument, +export function parseGeoRadiusByMemberArguments( + parser: CommandParser, key: RedisArgument, from: RedisArgument, radius: number, unit: GeoUnits, options?: GeoSearchOptions ) { - const args = [ - command, - key, - from, - radius.toString(), - unit - ]; + parser.pushKey(key); + parser.push(from, radius.toString(), unit); - pushGeoSearchOptions(args, options); - - return args; + parseGeoSearchOptions(parser, options); } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments: transformGeoRadiusByMemberArguments.bind(undefined, 'GEORADIUSBYMEMBER'), + parseCommand( + parser: CommandParser, + key: RedisArgument, + from: RedisArgument, + radius: number, + unit: GeoUnits, + options?: GeoSearchOptions + ) { + parser.push('GEORADIUSBYMEMBER'); + parseGeoRadiusByMemberArguments(parser, key, from, radius, unit, options); + }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts index abf10013973..bd4aa86dec1 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUSBYMEMBER_RO from './GEORADIUSBYMEMBER_RO'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUSBYMEMBER_RO', () => { it('transformArguments', () => { assert.deepEqual( - GEORADIUSBYMEMBER_RO.transformArguments('key', 'member', 3, 'm'), + parseArgs(GEORADIUSBYMEMBER_RO, 'key', 'member', 3, 'm'), ['GEORADIUSBYMEMBER_RO', 'key', 'member', '3', 'm'] ); }); diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts index 7f85ed15df7..335eea08133 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO.ts @@ -1,9 +1,13 @@ import { Command } from '../RESP/types'; -import GEORADIUSBYMEMBER, { transformGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; +import GEORADIUSBYMEMBER, { parseGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; export default { - FIRST_KEY_INDEX: GEORADIUSBYMEMBER.FIRST_KEY_INDEX, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments: transformGeoRadiusByMemberArguments.bind(undefined, 'GEORADIUSBYMEMBER_RO'), + parseCommand(...args: Parameters) { + const parser = args[0]; + parser.push('GEORADIUSBYMEMBER_RO'); + parseGeoRadiusByMemberArguments(...args); + }, transformReply: GEORADIUSBYMEMBER.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts index bcf91266365..52b31b03594 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.spec.ts @@ -3,6 +3,7 @@ import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUSBYMEMBER_RO_WITH from './GEORADIUSBYMEMBER_RO_WITH'; import { CommandArguments } from '../RESP/types'; import { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUSBYMEMBER_RO WITH', () => { it('transformArguments', () => { @@ -10,7 +11,7 @@ describe('GEORADIUSBYMEMBER_RO WITH', () => { expectedReply.preserve = ['WITHDIST']; assert.deepEqual( - GEORADIUSBYMEMBER_RO_WITH.transformArguments('key', 'member', 3, 'm', [ + parseArgs(GEORADIUSBYMEMBER_RO_WITH, 'key', 'member', 3, 'm', [ GEO_REPLY_WITH.DISTANCE ]), expectedReply diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts index 5fb945a1f9c..06835438016 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_RO_WITH.ts @@ -1,9 +1,13 @@ import { Command } from '../RESP/types'; -import GEORADIUSBYMEMBER_WITH, { transformGeoRadiusByMemberWithArguments } from './GEORADIUSBYMEMBER_WITH'; +import GEORADIUSBYMEMBER_WITH, { parseGeoRadiusByMemberWithArguments } from './GEORADIUSBYMEMBER_WITH'; export default { - FIRST_KEY_INDEX: GEORADIUSBYMEMBER_WITH.FIRST_KEY_INDEX, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments: transformGeoRadiusByMemberWithArguments.bind(undefined, 'GEORADIUSBYMEMBER_RO'), + parseCommand(...args: Parameters) { + const parser = args[0]; + parser.push('GEORADIUSBYMEMBER_RO'); + parseGeoRadiusByMemberWithArguments(...args); + }, transformReply: GEORADIUSBYMEMBER_WITH.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts index 3d44060f205..9edb08d1eae 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUSBYMEMBER_STORE from './GEORADIUSBYMEMBER_STORE'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUSBYMEMBER STORE', () => { describe('transformArguments', () => { it('STORE', () => { assert.deepEqual( - GEORADIUSBYMEMBER_STORE.transformArguments('key', 'member', 3, 'm', 'destination'), + parseArgs(GEORADIUSBYMEMBER_STORE, 'key', 'member', 3, 'm', 'destination'), ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'STORE', 'destination'] ); }); it('STOREDIST', () => { assert.deepEqual( - GEORADIUSBYMEMBER_STORE.transformArguments('key', 'member', 3, 'm', 'destination', { + parseArgs(GEORADIUSBYMEMBER_STORE, 'key', 'member', 3, 'm', 'destination', { STOREDIST: true }), ['GEORADIUSBYMEMBER', 'key', 'member', '3', 'm', 'STOREDIST', 'destination'] diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts index 90419963110..676df34dd5a 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_STORE.ts @@ -1,5 +1,6 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import GEORADIUSBYMEMBER, { transformGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; +import GEORADIUSBYMEMBER, { parseGeoRadiusByMemberArguments } from './GEORADIUSBYMEMBER'; import { GeoSearchOptions, GeoUnits } from './GEOSEARCH'; export interface GeoRadiusStoreOptions extends GeoSearchOptions { @@ -7,9 +8,9 @@ export interface GeoRadiusStoreOptions extends GeoSearchOptions { } export default { - FIRST_KEY_INDEX: GEORADIUSBYMEMBER.FIRST_KEY_INDEX, IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, from: RedisArgument, radius: number, @@ -17,15 +18,16 @@ export default { destination: RedisArgument, options?: GeoRadiusStoreOptions ) { - const args = transformGeoRadiusByMemberArguments('GEORADIUSBYMEMBER', key, from, radius, unit, options); + parser.push('GEORADIUSBYMEMBER') + parseGeoRadiusByMemberArguments(parser, key, from, radius, unit, options); if (options?.STOREDIST) { - args.push('STOREDIST', destination); + parser.push('STOREDIST'); + parser.pushKey(destination); } else { - args.push('STORE', destination); + parser.push('STORE'); + parser.pushKey(destination); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts index ffe3b2efff3..9d634d60656 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.spec.ts @@ -3,6 +3,7 @@ import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUSBYMEMBER_WITH from './GEORADIUSBYMEMBER_WITH'; import { CommandArguments } from '../RESP/types'; import { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUSBYMEMBER WITH', () => { it('transformArguments', () => { @@ -10,7 +11,7 @@ describe('GEORADIUSBYMEMBER WITH', () => { expectedReply.preserve = ['WITHDIST']; assert.deepEqual( - GEORADIUSBYMEMBER_WITH.transformArguments('key', 'member', 3, 'm', [ + parseArgs(GEORADIUSBYMEMBER_WITH, 'key', 'member', 3, 'm', [ GEO_REPLY_WITH.DISTANCE ]), expectedReply diff --git a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts index be9472a438f..eefae0b27a9 100644 --- a/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts +++ b/packages/client/lib/commands/GEORADIUSBYMEMBER_WITH.ts @@ -1,10 +1,11 @@ -import { RedisArgument, CommandArguments, Command } from '../RESP/types'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, Command } from '../RESP/types'; import GEORADIUSBYMEMBER from './GEORADIUSBYMEMBER'; -import { GeoSearchOptions, GeoUnits, pushGeoSearchOptions } from './GEOSEARCH'; +import { GeoSearchOptions, GeoUnits, parseGeoSearchOptions } from './GEOSEARCH'; import GEOSEARCH_WITH, { GeoReplyWith } from './GEOSEARCH_WITH'; -export function transformGeoRadiusByMemberWithArguments( - command: RedisArgument, +export function parseGeoRadiusByMemberWithArguments( + parser: CommandParser, key: RedisArgument, from: RedisArgument, radius: number, @@ -12,25 +13,27 @@ export function transformGeoRadiusByMemberWithArguments( replyWith: Array, options?: GeoSearchOptions ) { - const args: CommandArguments = [ - command, - key, - from, - radius.toString(), - unit - ]; + parser.pushKey(key); + parser.push(from, radius.toString(), unit); + parseGeoSearchOptions(parser, options); - pushGeoSearchOptions(args, options); - - args.push(...replyWith); - args.preserve = replyWith; - - return args; + parser.push(...replyWith); + parser.preserve = replyWith; } export default { - FIRST_KEY_INDEX: GEORADIUSBYMEMBER.FIRST_KEY_INDEX, IS_READ_ONLY: GEORADIUSBYMEMBER.IS_READ_ONLY, - transformArguments: transformGeoRadiusByMemberWithArguments.bind(undefined, 'GEORADIUSBYMEMBER'), + parseCommand( + parser: CommandParser, + key: RedisArgument, + from: RedisArgument, + radius: number, + unit: GeoUnits, + replyWith: Array, + options?: GeoSearchOptions + ) { + parser.push('GEORADIUSBYMEMBER'); + parseGeoRadiusByMemberWithArguments(parser, key, from, radius, unit, replyWith, options); + }, transformReply: GEOSEARCH_WITH.transformReply } as const satisfies Command; \ No newline at end of file diff --git a/packages/client/lib/commands/GEORADIUS_RO.spec.ts b/packages/client/lib/commands/GEORADIUS_RO.spec.ts index 43a2ef1d583..917eba3ab8e 100644 --- a/packages/client/lib/commands/GEORADIUS_RO.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_RO.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUS_RO from './GEORADIUS_RO'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUS_RO', () => { it('transformArguments', () => { assert.deepEqual( - GEORADIUS_RO.transformArguments('key', { + parseArgs(GEORADIUS_RO, 'key', { longitude: 1, latitude: 2 }, 3, 'm'), diff --git a/packages/client/lib/commands/GEORADIUS_RO.ts b/packages/client/lib/commands/GEORADIUS_RO.ts index be8bb4b530d..5db65d9dc9b 100644 --- a/packages/client/lib/commands/GEORADIUS_RO.ts +++ b/packages/client/lib/commands/GEORADIUS_RO.ts @@ -1,9 +1,12 @@ import { Command } from '../RESP/types'; -import GEORADIUS, { transformGeoRadiusArguments } from './GEORADIUS'; +import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS'; export default { - FIRST_KEY_INDEX: GEORADIUS.FIRST_KEY_INDEX, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments: transformGeoRadiusArguments.bind(undefined, 'GEORADIUS_RO'), + parseCommand(...args: Parameters) { + args[0].push('GEORADIUS_RO'); + parseGeoRadiusArguments(...args); + }, transformReply: GEORADIUS.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts b/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts index cb0540d8a18..01d79954b64 100644 --- a/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.spec.ts @@ -3,6 +3,7 @@ import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUS_RO_WITH from './GEORADIUS_RO_WITH'; import { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; import { CommandArguments } from '../RESP/types'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUS_RO WITH', () => { it('transformArguments', () => { @@ -10,7 +11,7 @@ describe('GEORADIUS_RO WITH', () => { expectedReply.preserve = ['WITHDIST']; assert.deepEqual( - GEORADIUS_RO_WITH.transformArguments('key', { + parseArgs(GEORADIUS_RO_WITH, 'key', { longitude: 1, latitude: 2 }, 3, 'm', [GEO_REPLY_WITH.DISTANCE]), diff --git a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts index 37cf594ce9d..cee1679382b 100644 --- a/packages/client/lib/commands/GEORADIUS_RO_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_RO_WITH.ts @@ -1,9 +1,13 @@ import { Command } from '../RESP/types'; -import GEORADIUS_WITH, { transformGeoRadiusWithArguments } from './GEORADIUS_WITH'; +import { parseGeoRadiusWithArguments } from './GEORADIUS_WITH'; +import GEORADIUS_WITH from './GEORADIUS_WITH'; export default { - FIRST_KEY_INDEX: GEORADIUS_WITH.FIRST_KEY_INDEX, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments: transformGeoRadiusWithArguments.bind(undefined, 'GEORADIUS_RO'), + parseCommand(...args: Parameters) { + args[0].push('GEORADIUS_RO'); + parseGeoRadiusWithArguments(...args); + }, transformReply: GEORADIUS_WITH.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUS_STORE.spec.ts b/packages/client/lib/commands/GEORADIUS_STORE.spec.ts index 04a7d28aa95..9a9bcf37bcf 100644 --- a/packages/client/lib/commands/GEORADIUS_STORE.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_STORE.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUS_STORE from './GEORADIUS_STORE'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUS STORE', () => { describe('transformArguments', () => { it('STORE', () => { assert.deepEqual( - GEORADIUS_STORE.transformArguments('key', { + parseArgs(GEORADIUS_STORE, 'key', { longitude: 1, latitude: 2 }, 3, 'm', 'destination'), @@ -16,7 +17,7 @@ describe('GEORADIUS STORE', () => { it('STOREDIST', () => { assert.deepEqual( - GEORADIUS_STORE.transformArguments('key', { + parseArgs(GEORADIUS_STORE, 'key', { longitude: 1, latitude: 2 }, 3, 'm', 'destination', { diff --git a/packages/client/lib/commands/GEORADIUS_STORE.ts b/packages/client/lib/commands/GEORADIUS_STORE.ts index 3a553ebf8be..18459d44217 100644 --- a/packages/client/lib/commands/GEORADIUS_STORE.ts +++ b/packages/client/lib/commands/GEORADIUS_STORE.ts @@ -1,5 +1,6 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import GEORADIUS, { transformGeoRadiusArguments } from './GEORADIUS'; +import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS'; import { GeoCoordinates, GeoSearchOptions, GeoUnits } from './GEOSEARCH'; export interface GeoRadiusStoreOptions extends GeoSearchOptions { @@ -7,9 +8,9 @@ export interface GeoRadiusStoreOptions extends GeoSearchOptions { } export default { - FIRST_KEY_INDEX: GEORADIUS.FIRST_KEY_INDEX, IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, from: GeoCoordinates, radius: number, @@ -17,15 +18,15 @@ export default { destination: RedisArgument, options?: GeoRadiusStoreOptions ) { - const args = transformGeoRadiusArguments('GEORADIUS', key, from, radius, unit, options); - + parser.push('GEORADIUS'); + parseGeoRadiusArguments(parser, key, from, radius, unit, options); if (options?.STOREDIST) { - args.push('STOREDIST', destination); + parser.push('STOREDIST'); + parser.pushKey(destination); } else { - args.push('STORE', destination); + parser.push('STORE'); + parser.pushKey(destination); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEORADIUS_WITH.spec.ts b/packages/client/lib/commands/GEORADIUS_WITH.spec.ts index bdbfc9c1f3a..f514c9be96f 100644 --- a/packages/client/lib/commands/GEORADIUS_WITH.spec.ts +++ b/packages/client/lib/commands/GEORADIUS_WITH.spec.ts @@ -3,6 +3,7 @@ import testUtils, { GLOBAL } from '../test-utils'; import GEORADIUS_WITH from './GEORADIUS_WITH'; import { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; import { CommandArguments } from '../RESP/types'; +import { parseArgs } from './generic-transformers'; describe('GEORADIUS WITH', () => { it('transformArguments', () => { @@ -10,7 +11,7 @@ describe('GEORADIUS WITH', () => { expectedReply.preserve = ['WITHDIST']; assert.deepEqual( - GEORADIUS_WITH.transformArguments('key', { + parseArgs(GEORADIUS_WITH, 'key', { longitude: 1, latitude: 2 }, 3, 'm', [GEO_REPLY_WITH.DISTANCE]), diff --git a/packages/client/lib/commands/GEORADIUS_WITH.ts b/packages/client/lib/commands/GEORADIUS_WITH.ts index d72d8d49322..ac4c8b7bb1b 100644 --- a/packages/client/lib/commands/GEORADIUS_WITH.ts +++ b/packages/client/lib/commands/GEORADIUS_WITH.ts @@ -1,33 +1,36 @@ -import { CommandArguments, Command, RedisArgument } from '../RESP/types'; -import GEORADIUS, { transformGeoRadiusArguments } from './GEORADIUS'; +import { CommandParser } from '../client/parser'; +import { Command, RedisArgument } from '../RESP/types'; +import GEORADIUS, { parseGeoRadiusArguments } from './GEORADIUS'; import { GeoCoordinates, GeoSearchOptions, GeoUnits } from './GEOSEARCH'; import GEOSEARCH_WITH, { GeoReplyWith } from './GEOSEARCH_WITH'; -export function transformGeoRadiusWithArguments( - command: RedisArgument, +export function parseGeoRadiusWithArguments( + parser: CommandParser, key: RedisArgument, from: GeoCoordinates, radius: number, unit: GeoUnits, replyWith: Array, - options?: GeoSearchOptions + options?: GeoSearchOptions, ) { - const args: CommandArguments = transformGeoRadiusArguments( - command, - key, - from, - radius, - unit, - options - ); - args.push(...replyWith); - args.preserve = replyWith; - return args; + parseGeoRadiusArguments(parser, key, from, radius, unit, options) + parser.pushVariadic(replyWith); + parser.preserve = replyWith; } export default { - FIRST_KEY_INDEX: GEORADIUS.FIRST_KEY_INDEX, IS_READ_ONLY: GEORADIUS.IS_READ_ONLY, - transformArguments: transformGeoRadiusWithArguments.bind(undefined, 'GEORADIUS'), + parseCommand( + parser: CommandParser, + key: RedisArgument, + from: GeoCoordinates, + radius: number, + unit: GeoUnits, + replyWith: Array, + options?: GeoSearchOptions + ) { + parser.push('GEORADIUS'); + parseGeoRadiusWithArguments(parser, key, from, radius, unit, replyWith, options); + }, transformReply: GEOSEARCH_WITH.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEOSEARCH.spec.ts b/packages/client/lib/commands/GEOSEARCH.spec.ts index 49f076880a6..4cd7e61a0ac 100644 --- a/packages/client/lib/commands/GEOSEARCH.spec.ts +++ b/packages/client/lib/commands/GEOSEARCH.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEOSEARCH from './GEOSEARCH'; +import { parseArgs } from './generic-transformers'; describe('GEOSEARCH', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,7 +9,7 @@ describe('GEOSEARCH', () => { describe('transformArguments', () => { it('FROMMEMBER, BYRADIUS, without options', () => { assert.deepEqual( - GEOSEARCH.transformArguments('key', 'member', { + parseArgs(GEOSEARCH, 'key', 'member', { radius: 1, unit: 'm' }), @@ -18,7 +19,7 @@ describe('GEOSEARCH', () => { it('FROMLONLAT, BYBOX, without options', () => { assert.deepEqual( - GEOSEARCH.transformArguments('key', { + parseArgs(GEOSEARCH, 'key', { longitude: 1, latitude: 2 }, { @@ -32,7 +33,7 @@ describe('GEOSEARCH', () => { it('with SORT', () => { assert.deepEqual( - GEOSEARCH.transformArguments('key', 'member', { + parseArgs(GEOSEARCH, 'key', 'member', { radius: 1, unit: 'm' }, { @@ -45,7 +46,7 @@ describe('GEOSEARCH', () => { describe('with COUNT', () => { it('number', () => { assert.deepEqual( - GEOSEARCH.transformArguments('key', 'member', { + parseArgs(GEOSEARCH, 'key', 'member', { radius: 1, unit: 'm' }, { @@ -57,7 +58,7 @@ describe('GEOSEARCH', () => { it('with ANY', () => { assert.deepEqual( - GEOSEARCH.transformArguments('key', 'member', { + parseArgs(GEOSEARCH, 'key', 'member', { radius: 1, unit: 'm' }, { diff --git a/packages/client/lib/commands/GEOSEARCH.ts b/packages/client/lib/commands/GEOSEARCH.ts index c4deaa37e6c..8c77fd89239 100644 --- a/packages/client/lib/commands/GEOSEARCH.ts +++ b/packages/client/lib/commands/GEOSEARCH.ts @@ -1,4 +1,5 @@ -import { RedisArgument, CommandArguments, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export type GeoUnits = 'm' | 'km' | 'mi' | 'ft'; @@ -22,30 +23,33 @@ export interface GeoSearchByBox { export type GeoSearchBy = GeoSearchByRadius | GeoSearchByBox; -export function pushGeoSearchArguments( - args: CommandArguments, +export function parseGeoSearchArguments( + parser: CommandParser, key: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, - options?: GeoSearchOptions + options?: GeoSearchOptions, + store?: RedisArgument ) { - args.push(key); + if (store !== undefined) { + parser.pushKey(store); + } + + parser.pushKey(key); if (typeof from === 'string' || from instanceof Buffer) { - args.push('FROMMEMBER', from); + parser.push('FROMMEMBER', from); } else { - args.push('FROMLONLAT', from.longitude.toString(), from.latitude.toString()); + parser.push('FROMLONLAT', from.longitude.toString(), from.latitude.toString()); } if ('radius' in by) { - args.push('BYRADIUS', by.radius.toString(), by.unit); + parser.push('BYRADIUS', by.radius.toString(), by.unit); } else { - args.push('BYBOX', by.width.toString(), by.height.toString(), by.unit); + parser.push('BYBOX', by.width.toString(), by.height.toString(), by.unit); } - pushGeoSearchOptions(args, options); - - return args; + parseGeoSearchOptions(parser, options); } export type GeoCountArgument = number | { @@ -58,37 +62,38 @@ export interface GeoSearchOptions { COUNT?: GeoCountArgument; } -export function pushGeoSearchOptions( - args: CommandArguments, +export function parseGeoSearchOptions( + parser: CommandParser, options?: GeoSearchOptions ) { if (options?.SORT) { - args.push(options.SORT); + parser.push(options.SORT); } if (options?.COUNT) { if (typeof options.COUNT === 'number') { - args.push('COUNT', options.COUNT.toString()); + parser.push('COUNT', options.COUNT.toString()); } else { - args.push('COUNT', options.COUNT.value.toString()); + parser.push('COUNT', options.COUNT.value.toString()); if (options.COUNT.ANY) { - args.push('ANY'); + parser.push('ANY'); } } } } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, options?: GeoSearchOptions ) { - return pushGeoSearchArguments(['GEOSEARCH'], key, from, by, options); + parser.push('GEOSEARCH'); + parseGeoSearchArguments(parser, key, from, by, options); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts b/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts index c66d3e8e45e..b8427ae0412 100644 --- a/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts +++ b/packages/client/lib/commands/GEOSEARCHSTORE.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEOSEARCHSTORE from './GEOSEARCHSTORE'; +import { parseArgs } from './generic-transformers'; describe('GEOSEARCHSTORE', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,7 +9,7 @@ describe('GEOSEARCHSTORE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - GEOSEARCHSTORE.transformArguments('source', 'destination', 'member', { + parseArgs(GEOSEARCHSTORE, 'source', 'destination', 'member', { radius: 1, unit: 'm' }), @@ -18,7 +19,7 @@ describe('GEOSEARCHSTORE', () => { it('with STOREDIST', () => { assert.deepEqual( - GEOSEARCHSTORE.transformArguments('destination', 'source', 'member', { + parseArgs(GEOSEARCHSTORE, 'destination', 'source', 'member', { radius: 1, unit: 'm' }, { diff --git a/packages/client/lib/commands/GEOSEARCHSTORE.ts b/packages/client/lib/commands/GEOSEARCHSTORE.ts index 15635560217..eb8e12abe6d 100644 --- a/packages/client/lib/commands/GEOSEARCHSTORE.ts +++ b/packages/client/lib/commands/GEOSEARCHSTORE.ts @@ -1,27 +1,27 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, pushGeoSearchArguments } from './GEOSEARCH'; +import { GeoSearchFrom, GeoSearchBy, GeoSearchOptions, parseGeoSearchArguments } from './GEOSEARCH'; export interface GeoSearchStoreOptions extends GeoSearchOptions { STOREDIST?: boolean; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, destination: RedisArgument, source: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, options?: GeoSearchStoreOptions ) { - const args = pushGeoSearchArguments(['GEOSEARCHSTORE', destination], source, from, by, options); + parser.push('GEOSEARCHSTORE'); + parseGeoSearchArguments(parser, source, from, by, options, destination); if (options?.STOREDIST) { - args.push('STOREDIST'); + parser.push('STOREDIST'); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts b/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts index e27fb295aaf..973e5d5827f 100644 --- a/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts +++ b/packages/client/lib/commands/GEOSEARCH_WITH.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEOSEARCH_WITH, { GEO_REPLY_WITH } from './GEOSEARCH_WITH'; import { CommandArguments } from '../RESP/types'; +import { parseArgs } from './generic-transformers'; describe('GEOSEARCH WITH', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -11,7 +12,7 @@ describe('GEOSEARCH WITH', () => { expectedReply.preserve = ['WITHDIST']; assert.deepEqual( - GEOSEARCH_WITH.transformArguments('key', 'member', { + parseArgs(GEOSEARCH_WITH, 'key', 'member', { radius: 1, unit: 'm' }, [GEO_REPLY_WITH.DISTANCE]), diff --git a/packages/client/lib/commands/GEOSEARCH_WITH.ts b/packages/client/lib/commands/GEOSEARCH_WITH.ts index 19088230f0f..65e3975b72f 100644 --- a/packages/client/lib/commands/GEOSEARCH_WITH.ts +++ b/packages/client/lib/commands/GEOSEARCH_WITH.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NumberReply, DoubleReply, UnwrapReply, Command } from '../RESP/types'; import GEOSEARCH, { GeoSearchBy, GeoSearchFrom, GeoSearchOptions } from './GEOSEARCH'; @@ -20,19 +21,18 @@ export interface GeoReplyWithMember { } export default { - FIRST_KEY_INDEX: GEOSEARCH.FIRST_KEY_INDEX, IS_READ_ONLY: GEOSEARCH.IS_READ_ONLY, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, from: GeoSearchFrom, by: GeoSearchBy, replyWith: Array, options?: GeoSearchOptions ) { - const args = GEOSEARCH.transformArguments(key, from, by, options); - args.push(...replyWith); - args.preserve = replyWith; - return args; + GEOSEARCH.parseCommand(parser, key, from, by, options); + parser.push(...replyWith); + parser.preserve = replyWith; }, transformReply( reply: UnwrapReply]>>>, diff --git a/packages/client/lib/commands/GET.spec.ts b/packages/client/lib/commands/GET.spec.ts index 4bd74183222..3e630d03e0b 100644 --- a/packages/client/lib/commands/GET.spec.ts +++ b/packages/client/lib/commands/GET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; import GET from './GET'; describe('GET', () => { it('transformArguments', () => { assert.deepEqual( - GET.transformArguments('key'), + parseArgs(GET, 'key'), ['GET', 'key'] ); }); diff --git a/packages/client/lib/commands/GET.ts b/packages/client/lib/commands/GET.ts index bb3db4f76d9..ca013752ae5 100644 --- a/packages/client/lib/commands/GET.ts +++ b/packages/client/lib/commands/GET.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['GET', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('GET'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GETBIT.spec.ts b/packages/client/lib/commands/GETBIT.spec.ts index ac39222b918..66d2798313c 100644 --- a/packages/client/lib/commands/GETBIT.spec.ts +++ b/packages/client/lib/commands/GETBIT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GETBIT from './GETBIT'; +import { parseArgs } from './generic-transformers'; describe('GETBIT', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - GETBIT.transformArguments('key', 0), + parseArgs(GETBIT, 'key', 0), ['GETBIT', 'key', '0'] ); }); diff --git a/packages/client/lib/commands/GETBIT.ts b/packages/client/lib/commands/GETBIT.ts index d8ece8f523a..023ba0fb607 100644 --- a/packages/client/lib/commands/GETBIT.ts +++ b/packages/client/lib/commands/GETBIT.ts @@ -1,11 +1,14 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; import { BitValue } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, offset: number) { - return ['GETBIT', key, offset.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, offset: number) { + parser.push('GETBIT'); + parser.pushKey(key); + parser.push(offset.toString()); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GETDEL.spec.ts b/packages/client/lib/commands/GETDEL.spec.ts index 311f15e554d..15ad5918008 100644 --- a/packages/client/lib/commands/GETDEL.spec.ts +++ b/packages/client/lib/commands/GETDEL.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GETDEL from './GETDEL'; +import { parseArgs } from './generic-transformers'; describe('GETDEL', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - GETDEL.transformArguments('key'), + parseArgs(GETDEL, 'key'), ['GETDEL', 'key'] ); }); diff --git a/packages/client/lib/commands/GETDEL.ts b/packages/client/lib/commands/GETDEL.ts index c11fd047df4..a39014109f1 100644 --- a/packages/client/lib/commands/GETDEL.ts +++ b/packages/client/lib/commands/GETDEL.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['GETDEL', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('GETDEL'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GETEX.spec.ts b/packages/client/lib/commands/GETEX.spec.ts index 302d034b961..5965d8f196f 100644 --- a/packages/client/lib/commands/GETEX.spec.ts +++ b/packages/client/lib/commands/GETEX.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GETEX from './GETEX'; +import { parseArgs } from './generic-transformers'; describe('GETEX', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,7 +9,7 @@ describe('GETEX', () => { describe('transformArguments', () => { it('EX | PX', () => { assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { type: 'EX', value: 1 }), @@ -18,7 +19,7 @@ describe('GETEX', () => { it('EX (backwards compatibility)', () => { assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { EX: 1 }), ['GETEX', 'key', 'EX', '1'] @@ -27,7 +28,7 @@ describe('GETEX', () => { it('PX (backwards compatibility)', () => { assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { PX: 1 }), ['GETEX', 'key', 'PX', '1'] @@ -37,7 +38,7 @@ describe('GETEX', () => { describe('EXAT | PXAT', () => { it('number', () => { assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { type: 'EXAT', value: 1 }), @@ -48,7 +49,7 @@ describe('GETEX', () => { it('date', () => { const d = new Date(); assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { EXAT: d }), ['GETEX', 'key', 'EXAT', Math.floor(d.getTime() / 1000).toString()] @@ -59,7 +60,7 @@ describe('GETEX', () => { describe('EXAT (backwards compatibility)', () => { it('number', () => { assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { EXAT: 1 }), ['GETEX', 'key', 'EXAT', '1'] @@ -69,7 +70,7 @@ describe('GETEX', () => { it('date', () => { const d = new Date(); assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { EXAT: d }), ['GETEX', 'key', 'EXAT', Math.floor(d.getTime() / 1000).toString()] @@ -80,7 +81,7 @@ describe('GETEX', () => { describe('PXAT (backwards compatibility)', () => { it('number', () => { assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { PXAT: 1 }), ['GETEX', 'key', 'PXAT', '1'] @@ -90,7 +91,7 @@ describe('GETEX', () => { it('date', () => { const d = new Date(); assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { PXAT: d }), ['GETEX', 'key', 'PXAT', d.getTime().toString()] @@ -100,7 +101,7 @@ describe('GETEX', () => { it('PERSIST (backwards compatibility)', () => { assert.deepEqual( - GETEX.transformArguments('key', { + parseArgs(GETEX, 'key', { PERSIST: true }), ['GETEX', 'key', 'PERSIST'] diff --git a/packages/client/lib/commands/GETEX.ts b/packages/client/lib/commands/GETEX.ts index 8244350eddb..e5ae0b691a7 100644 --- a/packages/client/lib/commands/GETEX.ts +++ b/packages/client/lib/commands/GETEX.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; import { transformEXAT, transformPXAT } from './generic-transformers'; @@ -37,42 +38,40 @@ export type GetExOptions = { }; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, options: GetExOptions) { - const args = ['GETEX', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options: GetExOptions) { + parser.push('GETEX'); + parser.pushKey(key); if ('type' in options) { switch (options.type) { case 'EX': case 'PX': - args.push(options.type, options.value.toString()); + parser.push(options.type, options.value.toString()); break; case 'EXAT': case 'PXAT': - args.push(options.type, transformEXAT(options.value)); + parser.push(options.type, transformEXAT(options.value)); break; case 'PERSIST': - args.push('PERSIST'); + parser.push('PERSIST'); break; } } else { if ('EX' in options) { - args.push('EX', options.EX.toString()); + parser.push('EX', options.EX.toString()); } else if ('PX' in options) { - args.push('PX', options.PX.toString()); + parser.push('PX', options.PX.toString()); } else if ('EXAT' in options) { - args.push('EXAT', transformEXAT(options.EXAT)); + parser.push('EXAT', transformEXAT(options.EXAT)); } else if ('PXAT' in options) { - args.push('PXAT', transformPXAT(options.PXAT)); + parser.push('PXAT', transformPXAT(options.PXAT)); } else { // PERSIST - args.push('PERSIST'); + parser.push('PERSIST'); } } - - return args; }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GETRANGE.spec.ts b/packages/client/lib/commands/GETRANGE.spec.ts index 2aac1ca16d9..8a8e7dde038 100644 --- a/packages/client/lib/commands/GETRANGE.spec.ts +++ b/packages/client/lib/commands/GETRANGE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GETRANGE from './GETRANGE'; +import { parseArgs } from './generic-transformers'; describe('GETRANGE', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - GETRANGE.transformArguments('key', 0, -1), + parseArgs(GETRANGE, 'key', 0, -1), ['GETRANGE', 'key', '0', '-1'] ); }); diff --git a/packages/client/lib/commands/GETRANGE.ts b/packages/client/lib/commands/GETRANGE.ts index e5357cd120b..ce0db6e3c03 100644 --- a/packages/client/lib/commands/GETRANGE.ts +++ b/packages/client/lib/commands/GETRANGE.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, start: number, end: number) { - return ['GETRANGE', key, start.toString(), end.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, start: number, end: number) { + parser.push('GETRANGE'); + parser.pushKey(key); + parser.push(start.toString(), end.toString()); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/GETSET.spec.ts b/packages/client/lib/commands/GETSET.spec.ts index 6583ec34f75..5b162c16cc4 100644 --- a/packages/client/lib/commands/GETSET.spec.ts +++ b/packages/client/lib/commands/GETSET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GETSET from './GETSET'; +import { parseArgs } from './generic-transformers'; describe('GETSET', () => { it('transformArguments', () => { assert.deepEqual( - GETSET.transformArguments('key', 'value'), + parseArgs(GETSET, 'key', 'value'), ['GETSET', 'key', 'value'] ); }); diff --git a/packages/client/lib/commands/GETSET.ts b/packages/client/lib/commands/GETSET.ts index bbe920181b2..1b3312548e4 100644 --- a/packages/client/lib/commands/GETSET.ts +++ b/packages/client/lib/commands/GETSET.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, value: RedisArgument) { - return ['GETSET', key, value]; + parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) { + parser.push('GETSET'); + parser.pushKey(key); + parser.push(value); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HDEL.spec.ts b/packages/client/lib/commands/HDEL.spec.ts index 9f69485d9fe..767d916e147 100644 --- a/packages/client/lib/commands/HDEL.spec.ts +++ b/packages/client/lib/commands/HDEL.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HDEL from './HDEL'; +import { parseArgs } from './generic-transformers'; describe('HDEL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - HDEL.transformArguments('key', 'field'), + parseArgs(HDEL, 'key', 'field'), ['HDEL', 'key', 'field'] ); }); it('array', () => { assert.deepEqual( - HDEL.transformArguments('key', ['1', '2']), + parseArgs(HDEL, 'key', ['1', '2']), ['HDEL', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/HDEL.ts b/packages/client/lib/commands/HDEL.ts index 64aa55edda6..713d19a9b2a 100644 --- a/packages/client/lib/commands/HDEL.ts +++ b/packages/client/lib/commands/HDEL.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, field: RedisVariadicArgument) { - return pushVariadicArguments(['HDEL', key], field); + parseCommand(parser: CommandParser, key: RedisArgument, field: RedisVariadicArgument) { + parser.push('HDEL'); + parser.pushKey(key); + parser.pushVariadic(field); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HELLO.spec.ts b/packages/client/lib/commands/HELLO.spec.ts index f7f117f18c7..5d11be344c1 100644 --- a/packages/client/lib/commands/HELLO.spec.ts +++ b/packages/client/lib/commands/HELLO.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HELLO from './HELLO'; +import { parseArgs } from './generic-transformers'; describe('HELLO', () => { testUtils.isVersionGreaterThanHook([6]); @@ -8,21 +9,21 @@ describe('HELLO', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - HELLO.transformArguments(), + parseArgs(HELLO), ['HELLO'] ); }); it('with protover', () => { assert.deepEqual( - HELLO.transformArguments(3), + parseArgs(HELLO, 3), ['HELLO', '3'] ); }); it('with protover, AUTH', () => { assert.deepEqual( - HELLO.transformArguments(3, { + parseArgs(HELLO, 3, { AUTH: { username: 'username', password: 'password' @@ -34,7 +35,7 @@ describe('HELLO', () => { it('with protover, SETNAME', () => { assert.deepEqual( - HELLO.transformArguments(3, { + parseArgs(HELLO, 3, { SETNAME: 'name' }), ['HELLO', '3', 'SETNAME', 'name'] @@ -43,7 +44,7 @@ describe('HELLO', () => { it('with protover, AUTH, SETNAME', () => { assert.deepEqual( - HELLO.transformArguments(3, { + parseArgs(HELLO, 3, { AUTH: { username: 'username', password: 'password' diff --git a/packages/client/lib/commands/HELLO.ts b/packages/client/lib/commands/HELLO.ts index 0fb2960d028..5d25998f987 100644 --- a/packages/client/lib/commands/HELLO.ts +++ b/packages/client/lib/commands/HELLO.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, RespVersions, TuplesToMapReply, BlobStringReply, NumberReply, ArrayReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; export interface HelloOptions { @@ -20,14 +21,14 @@ export type HelloReply = TuplesToMapReply<[ ]>; export default { - transformArguments(protover?: RespVersions, options?: HelloOptions) { - const args: Array = ['HELLO']; + parseCommand(parser: CommandParser, protover?: RespVersions, options?: HelloOptions) { + parser.push('HELLO'); if (protover) { - args.push(protover.toString()); + parser.push(protover.toString()); if (options?.AUTH) { - args.push( + parser.push( 'AUTH', options.AUTH.username, options.AUTH.password @@ -35,14 +36,12 @@ export default { } if (options?.SETNAME) { - args.push( + parser.push( 'SETNAME', options.SETNAME ); } } - - return args; }, transformReply: { 2: (reply: UnwrapReply>) => ({ diff --git a/packages/client/lib/commands/HEXISTS.spec.ts b/packages/client/lib/commands/HEXISTS.spec.ts index 69ca6fa765f..acd462ab7e2 100644 --- a/packages/client/lib/commands/HEXISTS.spec.ts +++ b/packages/client/lib/commands/HEXISTS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HEXISTS from './HEXISTS'; +import { parseArgs } from './generic-transformers'; describe('HEXISTS', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - HEXISTS.transformArguments('key', 'field'), + parseArgs(HEXISTS, 'key', 'field'), ['HEXISTS', 'key', 'field'] ); }); diff --git a/packages/client/lib/commands/HEXISTS.ts b/packages/client/lib/commands/HEXISTS.ts index dc7e937ee78..9bb517b7df4 100644 --- a/packages/client/lib/commands/HEXISTS.ts +++ b/packages/client/lib/commands/HEXISTS.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, field: RedisArgument) { - return ['HEXISTS', key, field]; + parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) { + parser.push('HEXISTS'); + parser.pushKey(key); + parser.push(field); }, transformReply: undefined as unknown as () => NumberReply<0 | 1> } as const satisfies Command; diff --git a/packages/client/lib/commands/HEXPIRE.spec.ts b/packages/client/lib/commands/HEXPIRE.spec.ts index 71c48b7e884..d28cc065ec9 100644 --- a/packages/client/lib/commands/HEXPIRE.spec.ts +++ b/packages/client/lib/commands/HEXPIRE.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HEXPIRE from './HEXPIRE'; +import { parseArgs } from './generic-transformers'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; describe('HEXPIRE', () => { @@ -9,21 +10,21 @@ describe('HEXPIRE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - HEXPIRE.transformArguments('key', 'field', 1), + parseArgs(HEXPIRE, 'key', 'field', 1), ['HEXPIRE', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - HEXPIRE.transformArguments('key', ['field1', 'field2'], 1), + parseArgs(HEXPIRE, 'key', ['field1', 'field2'], 1), ['HEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); it('with set option', () => { assert.deepEqual( - HEXPIRE.transformArguments('key', ['field1'], 1, 'NX'), + parseArgs(HEXPIRE, 'key', ['field1'], 1, 'NX'), ['HEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1'] ); }); diff --git a/packages/client/lib/commands/HEXPIRE.ts b/packages/client/lib/commands/HEXPIRE.ts index 34b52c1db68..55e2f5a9be1 100644 --- a/packages/client/lib/commands/HEXPIRE.ts +++ b/packages/client/lib/commands/HEXPIRE.ts @@ -1,5 +1,6 @@ -import { Command, RedisArgument } from '../RESP/types'; -import { pushVariadicArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { ArrayReply, Command, RedisArgument } from '../RESP/types'; +import { RedisVariadicArgument } from './generic-transformers'; export const HASH_EXPIRATION = { /** The field does not exist */ @@ -11,25 +12,28 @@ export const HASH_EXPIRATION = { /** Field deleted because the specified expiration time is in the past */ DELETED: 2 } as const; - + export type HashExpiration = typeof HASH_EXPIRATION[keyof typeof HASH_EXPIRATION]; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, - fields: RedisArgument | Array, + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument, seconds: number, - mode?: 'NX' | 'XX' | 'GT' | 'LT', + mode?: 'NX' | 'XX' | 'GT' | 'LT' ) { - const args = ['HEXPIRE', key, seconds.toString()]; - + parser.push('HEXPIRE'); + parser.pushKey(key); + parser.push(seconds.toString()); + if (mode) { - args.push(mode); + parser.push(mode); } - args.push('FIELDS'); + parser.push('FIELDS'); - return pushVariadicArgument(args, fields); + parser.pushVariadicWithLength(fields); }, - transformReply: undefined as unknown as () => Array + transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HEXPIREAT.spec.ts b/packages/client/lib/commands/HEXPIREAT.spec.ts index 1f87300214c..c7cc9fe749b 100644 --- a/packages/client/lib/commands/HEXPIREAT.spec.ts +++ b/packages/client/lib/commands/HEXPIREAT.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HEXPIREAT from './HEXPIREAT'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('HEXPIREAT', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HEXPIREAT', () => { describe('transformArguments', () => { it('string + number', () => { assert.deepEqual( - HEXPIREAT.transformArguments('key', 'field', 1), + parseArgs(HEXPIREAT, 'key', 'field', 1), ['HEXPIREAT', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array + number', () => { assert.deepEqual( - HEXPIREAT.transformArguments('key', ['field1', 'field2'], 1), + parseArgs(HEXPIREAT, 'key', ['field1', 'field2'], 1), ['HEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); @@ -25,14 +26,14 @@ describe('HEXPIREAT', () => { const d = new Date(); assert.deepEqual( - HEXPIREAT.transformArguments('key', ['field1'], d), + parseArgs(HEXPIREAT, 'key', ['field1'], d), ['HEXPIREAT', 'key', Math.floor(d.getTime() / 1000).toString(), 'FIELDS', '1', 'field1'] ); }); it('with set option', () => { assert.deepEqual( - HEXPIREAT.transformArguments('key', 'field1', 1, 'GT'), + parseArgs(HEXPIREAT, 'key', 'field1', 1, 'GT'), ['HEXPIREAT', 'key', '1', 'GT', 'FIELDS', '1', 'field1'] ); }); diff --git a/packages/client/lib/commands/HEXPIREAT.ts b/packages/client/lib/commands/HEXPIREAT.ts index 5a49951f1cd..1370f2ecd65 100644 --- a/packages/client/lib/commands/HEXPIREAT.ts +++ b/packages/client/lib/commands/HEXPIREAT.ts @@ -1,28 +1,26 @@ -import { Command, RedisArgument } from '../RESP/types'; -import { pushVariadicArgument, RedisVariadicArgument, transformEXAT } from './generic-transformers'; -import { HashExpiration } from './HEXPIRE'; +import { CommandParser } from '../client/parser'; +import { RedisVariadicArgument, transformEXAT } from './generic-transformers'; +import { ArrayReply, Command, NumberReply, RedisArgument } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument, timestamp: number | Date, mode?: 'NX' | 'XX' | 'GT' | 'LT' ) { - const args = [ - 'HEXPIREAT', - key, - transformEXAT(timestamp) - ]; - + parser.push('HEXPIREAT'); + parser.pushKey(key); + parser.push(transformEXAT(timestamp)); + if (mode) { - args.push(mode); + parser.push(mode); } - - args.push('FIELDS') - - return pushVariadicArgument(args, fields); + + parser.push('FIELDS') + + parser.pushVariadicWithLength(fields); }, - transformReply: undefined as unknown as () => Array + transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HEXPIRETIME.spec.ts b/packages/client/lib/commands/HEXPIRETIME.spec.ts index 2335ec91726..32a8730e8a9 100644 --- a/packages/client/lib/commands/HEXPIRETIME.spec.ts +++ b/packages/client/lib/commands/HEXPIRETIME.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HEXPIRETIME, { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('HEXPIRETIME', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -8,14 +9,14 @@ describe('HEXPIRETIME', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - HEXPIRETIME.transformArguments('key', 'field'), + parseArgs(HEXPIRETIME, 'key', 'field'), ['HEXPIRETIME', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - HEXPIRETIME.transformArguments('key', ['field1', 'field2']), + parseArgs(HEXPIRETIME, 'key', ['field1', 'field2']), ['HEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HEXPIRETIME.ts b/packages/client/lib/commands/HEXPIRETIME.ts index 7edf1309002..697d327db16 100644 --- a/packages/client/lib/commands/HEXPIRETIME.ts +++ b/packages/client/lib/commands/HEXPIRETIME.ts @@ -1,5 +1,6 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, Command, NumberReply, RedisArgument } from '../RESP/types'; -import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export const HASH_EXPIRATION_TIME = { /** The field does not exist */ @@ -9,10 +10,16 @@ export const HASH_EXPIRATION_TIME = { } as const; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { - return pushVariadicArgument(['HEXPIRETIME', key, 'FIELDS'], fields); + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument + ) { + parser.push('HEXPIRETIME'); + parser.pushKey(key); + parser.push('FIELDS'); + parser.pushVariadicWithLength(fields); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HGET.spec.ts b/packages/client/lib/commands/HGET.spec.ts index 397f22b5604..47061876aea 100644 --- a/packages/client/lib/commands/HGET.spec.ts +++ b/packages/client/lib/commands/HGET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HGET from './HGET'; +import { parseArgs } from './generic-transformers'; describe('HGET', () => { it('transformArguments', () => { assert.deepEqual( - HGET.transformArguments('key', 'field'), + parseArgs(HGET, 'key', 'field'), ['HGET', 'key', 'field'] ); }); diff --git a/packages/client/lib/commands/HGET.ts b/packages/client/lib/commands/HGET.ts index d83f84e24fa..fcd9334eb0a 100644 --- a/packages/client/lib/commands/HGET.ts +++ b/packages/client/lib/commands/HGET.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, field: RedisArgument) { - return ['HGET', key, field]; + parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) { + parser.push('HGET'); + parser.pushKey(key); + parser.push(field); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HGETALL.ts b/packages/client/lib/commands/HGETALL.ts index f1f0ac50bcb..a2c3011c4c2 100644 --- a/packages/client/lib/commands/HGETALL.ts +++ b/packages/client/lib/commands/HGETALL.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, MapReply, BlobStringReply, Command } from '../RESP/types'; import { transformTuplesReply } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['HGETALL', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('HGETALL'); + parser.pushKey(key); }, TRANSFORM_LEGACY_REPLY: true, transformReply: { diff --git a/packages/client/lib/commands/HINCRBY.spec.ts b/packages/client/lib/commands/HINCRBY.spec.ts index 7718fe955eb..ad382d97a99 100644 --- a/packages/client/lib/commands/HINCRBY.spec.ts +++ b/packages/client/lib/commands/HINCRBY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HINCRBY from './HINCRBY'; +import { parseArgs } from './generic-transformers'; describe('HINCRBY', () => { it('transformArguments', () => { assert.deepEqual( - HINCRBY.transformArguments('key', 'field', 1), + parseArgs(HINCRBY, 'key', 'field', 1), ['HINCRBY', 'key', 'field', '1'] ); }); diff --git a/packages/client/lib/commands/HINCRBY.ts b/packages/client/lib/commands/HINCRBY.ts index cb7f62ebef5..3638e408f7d 100644 --- a/packages/client/lib/commands/HINCRBY.ts +++ b/packages/client/lib/commands/HINCRBY.ts @@ -1,18 +1,16 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, field: RedisArgument, increment: number ) { - return [ - 'HINCRBY', - key, - field, - increment.toString() - ]; + parser.push('HINCRBY'); + parser.pushKey(key); + parser.push(field, increment.toString()); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HINCRBYFLOAT.spec.ts b/packages/client/lib/commands/HINCRBYFLOAT.spec.ts index 6c265dc6d10..2edbd6f9477 100644 --- a/packages/client/lib/commands/HINCRBYFLOAT.spec.ts +++ b/packages/client/lib/commands/HINCRBYFLOAT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HINCRBYFLOAT from './HINCRBYFLOAT'; +import { parseArgs } from './generic-transformers'; describe('HINCRBYFLOAT', () => { it('transformArguments', () => { assert.deepEqual( - HINCRBYFLOAT.transformArguments('key', 'field', 1.5), + parseArgs(HINCRBYFLOAT, 'key', 'field', 1.5), ['HINCRBYFLOAT', 'key', 'field', '1.5'] ); }); diff --git a/packages/client/lib/commands/HINCRBYFLOAT.ts b/packages/client/lib/commands/HINCRBYFLOAT.ts index a4eea75c827..6d527583c71 100644 --- a/packages/client/lib/commands/HINCRBYFLOAT.ts +++ b/packages/client/lib/commands/HINCRBYFLOAT.ts @@ -1,18 +1,16 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, field: RedisArgument, increment: number ) { - return [ - 'HINCRBYFLOAT', - key, - field, - increment.toString() - ]; + parser.push('HINCRBYFLOAT'); + parser.pushKey(key); + parser.push(field, increment.toString()); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HKEYS.spec.ts b/packages/client/lib/commands/HKEYS.spec.ts index dada7b4d6fd..58445696d20 100644 --- a/packages/client/lib/commands/HKEYS.spec.ts +++ b/packages/client/lib/commands/HKEYS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HKEYS from './HKEYS'; +import { parseArgs } from './generic-transformers'; describe('HKEYS', () => { it('transformArguments', () => { assert.deepEqual( - HKEYS.transformArguments('key'), + parseArgs(HKEYS, 'key'), ['HKEYS', 'key'] ); }); diff --git a/packages/client/lib/commands/HKEYS.ts b/packages/client/lib/commands/HKEYS.ts index 00af43f7a40..f07a1ac127f 100644 --- a/packages/client/lib/commands/HKEYS.ts +++ b/packages/client/lib/commands/HKEYS.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['HKEYS', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('HKEYS') + parser.pushKey(key); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HLEN.spec.ts b/packages/client/lib/commands/HLEN.spec.ts index 2457a261299..640e461ad07 100644 --- a/packages/client/lib/commands/HLEN.spec.ts +++ b/packages/client/lib/commands/HLEN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HLEN from './HLEN'; +import { parseArgs } from './generic-transformers'; describe('HLEN', () => { it('transformArguments', () => { assert.deepEqual( - HLEN.transformArguments('key'), + parseArgs(HLEN, 'key'), ['HLEN', 'key'] ); }); diff --git a/packages/client/lib/commands/HLEN.ts b/packages/client/lib/commands/HLEN.ts index 8f156d303e2..e3b89da3e7d 100644 --- a/packages/client/lib/commands/HLEN.ts +++ b/packages/client/lib/commands/HLEN.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['HLEN', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('HLEN'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HMGET.spec.ts b/packages/client/lib/commands/HMGET.spec.ts index 99d94a6d375..8cc90e4abd5 100644 --- a/packages/client/lib/commands/HMGET.spec.ts +++ b/packages/client/lib/commands/HMGET.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HMGET from './HMGET'; +import { parseArgs } from './generic-transformers'; describe('HMGET', () => { - describe('transformArguments', () => { + describe('parseCommand', () => { it('string', () => { assert.deepEqual( - HMGET.transformArguments('key', 'field'), + parseArgs(HMGET, 'key', 'field'), ['HMGET', 'key', 'field'] ); }); it('array', () => { assert.deepEqual( - HMGET.transformArguments('key', ['field1', 'field2']), + parseArgs(HMGET, 'key', ['field1', 'field2']), ['HMGET', 'key', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HMGET.ts b/packages/client/lib/commands/HMGET.ts index df28a64be09..51ba937339f 100644 --- a/packages/client/lib/commands/HMGET.ts +++ b/packages/client/lib/commands/HMGET.ts @@ -1,14 +1,14 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( - key: RedisArgument, - fields: RedisVariadicArgument - ) { - return pushVariadicArguments(['HMGET', key], fields); + parseCommand(parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument) { + parser.push('HMGET'); + parser.pushKey(key); + parser.pushVariadic(fields); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HPERSIST.spec.ts b/packages/client/lib/commands/HPERSIST.spec.ts index 05e225e8ead..0b317977cbf 100644 --- a/packages/client/lib/commands/HPERSIST.spec.ts +++ b/packages/client/lib/commands/HPERSIST.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HPERSIST from './HPERSIST'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('HPERSIST', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HPERSIST', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - HPERSIST.transformArguments('key', 'field'), + parseArgs(HPERSIST, 'key', 'field'), ['HPERSIST', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - HPERSIST.transformArguments('key', ['field1', 'field2']), + parseArgs(HPERSIST, 'key', ['field1', 'field2']), ['HPERSIST', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HPERSIST.ts b/packages/client/lib/commands/HPERSIST.ts index 3843fd80a5e..fd0f320e65a 100644 --- a/packages/client/lib/commands/HPERSIST.ts +++ b/packages/client/lib/commands/HPERSIST.ts @@ -1,11 +1,17 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; -import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - IS_READ_ONLY: true, - transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { - return pushVariadicArgument(['HPERSIST', key, 'FIELDS'], fields); + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument + ) { + parser.push('HPERSIST'); + parser.pushKey(key); + parser.push('FIELDS'); + parser.pushVariadicWithLength(fields); }, transformReply: undefined as unknown as () => ArrayReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HPEXPIRE.spec.ts b/packages/client/lib/commands/HPEXPIRE.spec.ts index febcb0bc96b..2f68fb9b7f3 100644 --- a/packages/client/lib/commands/HPEXPIRE.spec.ts +++ b/packages/client/lib/commands/HPEXPIRE.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HPEXPIRE from './HPEXPIRE'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('HEXPIRE', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,21 +10,21 @@ describe('HEXPIRE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - HPEXPIRE.transformArguments('key', 'field', 1), + parseArgs(HPEXPIRE, 'key', 'field', 1), ['HPEXPIRE', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - HPEXPIRE.transformArguments('key', ['field1', 'field2'], 1), + parseArgs(HPEXPIRE, 'key', ['field1', 'field2'], 1), ['HPEXPIRE', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); it('with set option', () => { assert.deepEqual( - HPEXPIRE.transformArguments('key', ['field1'], 1, 'NX'), + parseArgs(HPEXPIRE, 'key', ['field1'], 1, 'NX'), ['HPEXPIRE', 'key', '1', 'NX', 'FIELDS', '1', 'field1'] ); }); diff --git a/packages/client/lib/commands/HPEXPIRE.ts b/packages/client/lib/commands/HPEXPIRE.ts index 58624f9163a..34513c34e3b 100644 --- a/packages/client/lib/commands/HPEXPIRE.ts +++ b/packages/client/lib/commands/HPEXPIRE.ts @@ -1,24 +1,27 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, Command, NullReply, RedisArgument } from '../RESP/types'; -import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; -import { HashExpiration } from "./HEXPIRE"; +import { RedisVariadicArgument } from './generic-transformers'; +import { HashExpiration } from './HEXPIRE'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument, ms: number, mode?: 'NX' | 'XX' | 'GT' | 'LT', ) { - const args = ['HPEXPIRE', key, ms.toString()]; - + parser.push('HPEXPIRE'); + parser.pushKey(key); + parser.push(ms.toString()); + if (mode) { - args.push(mode); + parser.push(mode); } - - args.push('FIELDS') - - return pushVariadicArgument(args, fields); + + parser.push('FIELDS') + + parser.pushVariadicWithLength(fields); }, transformReply: undefined as unknown as () => ArrayReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HPEXPIREAT.spec.ts b/packages/client/lib/commands/HPEXPIREAT.spec.ts index f91bf967cf8..7c369980bf4 100644 --- a/packages/client/lib/commands/HPEXPIREAT.spec.ts +++ b/packages/client/lib/commands/HPEXPIREAT.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HPEXPIREAT from './HPEXPIREAT'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('HPEXPIREAT', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HPEXPIREAT', () => { describe('transformArguments', () => { it('string + number', () => { assert.deepEqual( - HPEXPIREAT.transformArguments('key', 'field', 1), + parseArgs(HPEXPIREAT, 'key', 'field', 1), ['HPEXPIREAT', 'key', '1', 'FIELDS', '1', 'field'] ); }); it('array + number', () => { assert.deepEqual( - HPEXPIREAT.transformArguments('key', ['field1', 'field2'], 1), + parseArgs(HPEXPIREAT, 'key', ['field1', 'field2'], 1), ['HPEXPIREAT', 'key', '1', 'FIELDS', '2', 'field1', 'field2'] ); }); @@ -24,14 +25,14 @@ describe('HPEXPIREAT', () => { it('date', () => { const d = new Date(); assert.deepEqual( - HPEXPIREAT.transformArguments('key', ['field1'], d), + parseArgs(HPEXPIREAT, 'key', ['field1'], d), ['HPEXPIREAT', 'key', d.getTime().toString(), 'FIELDS', '1', 'field1'] ); }); it('with set option', () => { assert.deepEqual( - HPEXPIREAT.transformArguments('key', ['field1'], 1, 'XX'), + parseArgs(HPEXPIREAT, 'key', ['field1'], 1, 'XX'), ['HPEXPIREAT', 'key', '1', 'XX', 'FIELDS', '1', 'field1'] ); }); diff --git a/packages/client/lib/commands/HPEXPIREAT.ts b/packages/client/lib/commands/HPEXPIREAT.ts index a6250d99432..14288d7ae90 100644 --- a/packages/client/lib/commands/HPEXPIREAT.ts +++ b/packages/client/lib/commands/HPEXPIREAT.ts @@ -1,24 +1,28 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, Command, NullReply, RedisArgument } from '../RESP/types'; -import { pushVariadicArgument, RedisVariadicArgument, transformPXAT } from './generic-transformers'; +import { RedisVariadicArgument, transformPXAT } from './generic-transformers'; import { HashExpiration } from './HEXPIRE'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument, timestamp: number | Date, mode?: 'NX' | 'XX' | 'GT' | 'LT' ) { - const args = ['HPEXPIREAT', key, transformPXAT(timestamp)]; + parser.push('HPEXPIREAT'); + parser.pushKey(key); + parser.push(transformPXAT(timestamp)); if (mode) { - args.push(mode); + parser.push(mode); } - args.push('FIELDS') + parser.push('FIELDS') - return pushVariadicArgument(args, fields); + parser.pushVariadicWithLength(fields); }, transformReply: undefined as unknown as () => ArrayReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HPEXPIRETIME.spec.ts b/packages/client/lib/commands/HPEXPIRETIME.spec.ts index a66988c428c..5673a725afc 100644 --- a/packages/client/lib/commands/HPEXPIRETIME.spec.ts +++ b/packages/client/lib/commands/HPEXPIRETIME.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HPEXPIRETIME from './HPEXPIRETIME'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('HPEXPIRETIME', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HPEXPIRETIME', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - HPEXPIRETIME.transformArguments('key', 'field'), + parseArgs(HPEXPIRETIME, 'key', 'field'), ['HPEXPIRETIME', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - HPEXPIRETIME.transformArguments('key', ['field1', 'field2']), + parseArgs(HPEXPIRETIME, 'key', ['field1', 'field2']), ['HPEXPIRETIME', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HPEXPIRETIME.ts b/packages/client/lib/commands/HPEXPIRETIME.ts index acdccf25119..cacce25a85f 100644 --- a/packages/client/lib/commands/HPEXPIRETIME.ts +++ b/packages/client/lib/commands/HPEXPIRETIME.ts @@ -1,11 +1,18 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; -import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { - return pushVariadicArgument(['HPEXPIRETIME', key, 'FIELDS'], fields); + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument, + ) { + parser.push('HPEXPIRETIME'); + parser.pushKey(key); + parser.push('FIELDS'); + parser.pushVariadicWithLength(fields); }, transformReply: undefined as unknown as () => ArrayReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HPTTL.spec.ts b/packages/client/lib/commands/HPTTL.spec.ts index 7280ef841ca..baaa11b19c8 100644 --- a/packages/client/lib/commands/HPTTL.spec.ts +++ b/packages/client/lib/commands/HPTTL.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HPTTL from './HPTTL'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('HPTTL', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,14 +10,14 @@ describe('HPTTL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - HPTTL.transformArguments('key', 'field'), + parseArgs(HPTTL, 'key', 'field'), ['HPTTL', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - HPTTL.transformArguments('key', ['field1', 'field2']), + parseArgs(HPTTL, 'key', ['field1', 'field2']), ['HPTTL', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); diff --git a/packages/client/lib/commands/HPTTL.ts b/packages/client/lib/commands/HPTTL.ts index 4ab069db74e..b9cd54a850d 100644 --- a/packages/client/lib/commands/HPTTL.ts +++ b/packages/client/lib/commands/HPTTL.ts @@ -1,11 +1,18 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; -import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { - return pushVariadicArgument(['HPTTL', key, 'FIELDS'], fields); + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument + ) { + parser.push('HPTTL'); + parser.pushKey(key); + parser.push('FIELDS'); + parser.pushVariadicWithLength(fields); }, transformReply: undefined as unknown as () => ArrayReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HRANDFIELD.spec.ts b/packages/client/lib/commands/HRANDFIELD.spec.ts index 33f2d281803..151636057a0 100644 --- a/packages/client/lib/commands/HRANDFIELD.spec.ts +++ b/packages/client/lib/commands/HRANDFIELD.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HRANDFIELD from './HRANDFIELD'; +import { parseArgs } from './generic-transformers'; describe('HRANDFIELD', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - HRANDFIELD.transformArguments('key'), + parseArgs(HRANDFIELD, 'key'), ['HRANDFIELD', 'key'] ); }); diff --git a/packages/client/lib/commands/HRANDFIELD.ts b/packages/client/lib/commands/HRANDFIELD.ts index be878e244a0..3383b94dcb2 100644 --- a/packages/client/lib/commands/HRANDFIELD.ts +++ b/packages/client/lib/commands/HRANDFIELD.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['HRANDFIELD', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('HRANDFIELD'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts b/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts index 99788dc4962..ee3fc984d55 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HRANDFIELD_COUNT from './HRANDFIELD_COUNT'; +import { parseArgs } from './generic-transformers'; describe('HRANDFIELD COUNT', () => { testUtils.isVersionGreaterThanHook([6, 2, 5]); it('transformArguments', () => { assert.deepEqual( - HRANDFIELD_COUNT.transformArguments('key', 1), + parseArgs(HRANDFIELD_COUNT, 'key', 1), ['HRANDFIELD', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT.ts b/packages/client/lib/commands/HRANDFIELD_COUNT.ts index 4b6f42a115b..62abe97e350 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, count: number) { - return ['HRANDFIELD', key, count.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + parser.push('HRANDFIELD'); + parser.pushKey(key); + parser.push(count.toString()); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts index ab36183c4ad..aa8ebad1b93 100644 --- a/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts +++ b/packages/client/lib/commands/HRANDFIELD_COUNT_WITHVALUES.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, UnwrapReply, Command } from '../RESP/types'; export type HRandFieldCountWithValuesReply = Array<{ @@ -6,10 +7,11 @@ export type HRandFieldCountWithValuesReply = Array<{ }>; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, count: number) { - return ['HRANDFIELD', key, count.toString(), 'WITHVALUES']; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + parser.push('HRANDFIELD'); + parser.pushKey(key); + parser.push(count.toString(), 'WITHVALUES'); }, transformReply: { 2: (rawReply: UnwrapReply>) => { diff --git a/packages/client/lib/commands/HSCAN.spec.ts b/packages/client/lib/commands/HSCAN.spec.ts index a5f3cdca16c..9e489f6190d 100644 --- a/packages/client/lib/commands/HSCAN.spec.ts +++ b/packages/client/lib/commands/HSCAN.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; import HSCAN from './HSCAN'; describe('HSCAN', () => { describe('transformArguments', () => { it('cusror only', () => { assert.deepEqual( - HSCAN.transformArguments('key', '0'), + parseArgs(HSCAN, 'key', '0'), ['HSCAN', 'key', '0'] ); }); it('with MATCH', () => { assert.deepEqual( - HSCAN.transformArguments('key', '0', { + parseArgs(HSCAN, 'key', '0', { MATCH: 'pattern' }), ['HSCAN', 'key', '0', 'MATCH', 'pattern'] @@ -22,7 +23,7 @@ describe('HSCAN', () => { it('with COUNT', () => { assert.deepEqual( - HSCAN.transformArguments('key', '0', { + parseArgs(HSCAN, 'key', '0', { COUNT: 1 }), ['HSCAN', 'key', '0', 'COUNT', '1'] @@ -31,7 +32,7 @@ describe('HSCAN', () => { it('with MATCH & COUNT', () => { assert.deepEqual( - HSCAN.transformArguments('key', '0', { + parseArgs(HSCAN, 'key', '0', { MATCH: 'pattern', COUNT: 1 }), diff --git a/packages/client/lib/commands/HSCAN.ts b/packages/client/lib/commands/HSCAN.ts index db52db99fef..e1e40663a07 100644 --- a/packages/client/lib/commands/HSCAN.ts +++ b/packages/client/lib/commands/HSCAN.ts @@ -1,5 +1,6 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -import { ScanCommonOptions, pushScanArguments } from './SCAN'; +import { ScanCommonOptions, parseScanArguments } from './SCAN'; export interface HScanEntry { field: BlobStringReply; @@ -7,14 +8,16 @@ export interface HScanEntry { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, cursor: RedisArgument, options?: ScanCommonOptions ) { - return pushScanArguments(['HSCAN', key], cursor, options); + parser.push('HSCAN'); + parser.pushKey(key); + parseScanArguments(parser, cursor, options); }, transformReply([cursor, rawEntries]: [BlobStringReply, Array]) { const entries = []; diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts b/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts index 1283a116dc5..83a452a6897 100644 --- a/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts +++ b/packages/client/lib/commands/HSCAN_NOVALUES.spec.ts @@ -1,7 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HSCAN_NOVALUES from './HSCAN_NOVALUES'; -import { BlobStringReply } from '../RESP/types'; +import { parseArgs } from './generic-transformers'; describe('HSCAN_NOVALUES', () => { testUtils.isVersionGreaterThanHook([7,4]); @@ -9,14 +9,14 @@ describe('HSCAN_NOVALUES', () => { describe('transformArguments', () => { it('cusror only', () => { assert.deepEqual( - HSCAN_NOVALUES.transformArguments('key', '0'), + parseArgs(HSCAN_NOVALUES, 'key', '0'), ['HSCAN', 'key', '0', 'NOVALUES'] ); }); it('with MATCH', () => { assert.deepEqual( - HSCAN_NOVALUES.transformArguments('key', '0', { + parseArgs(HSCAN_NOVALUES, 'key', '0', { MATCH: 'pattern' }), ['HSCAN', 'key', '0', 'MATCH', 'pattern', 'NOVALUES'] @@ -25,7 +25,7 @@ describe('HSCAN_NOVALUES', () => { it('with COUNT', () => { assert.deepEqual( - HSCAN_NOVALUES.transformArguments('key', '0', { + parseArgs(HSCAN_NOVALUES, 'key', '0', { COUNT: 1 }), ['HSCAN', 'key', '0', 'COUNT', '1', 'NOVALUES'] @@ -34,7 +34,7 @@ describe('HSCAN_NOVALUES', () => { it('with MATCH & COUNT', () => { assert.deepEqual( - HSCAN_NOVALUES.transformArguments('key', '0', { + parseArgs(HSCAN_NOVALUES, 'key', '0', { MATCH: 'pattern', COUNT: 1 }), diff --git a/packages/client/lib/commands/HSCAN_NOVALUES.ts b/packages/client/lib/commands/HSCAN_NOVALUES.ts index 35ff861338c..eff61a7aab0 100644 --- a/packages/client/lib/commands/HSCAN_NOVALUES.ts +++ b/packages/client/lib/commands/HSCAN_NOVALUES.ts @@ -1,17 +1,13 @@ -import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -import { ScanCommonOptions, pushScanArguments } from './SCAN'; +import { BlobStringReply, Command } from '../RESP/types'; +import HSCAN from './HSCAN'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( - key: RedisArgument, - cursor: RedisArgument, - options?: ScanCommonOptions - ) { - const args = pushScanArguments(['HSCAN', key], cursor, options); - args.push('NOVALUES'); - return args; + parseCommand(...args: Parameters) { + const parser = args[0]; + + HSCAN.parseCommand(...args); + parser.push('NOVALUES'); }, transformReply([cursor, fields]: [BlobStringReply, Array]) { return { diff --git a/packages/client/lib/commands/HSET.spec.ts b/packages/client/lib/commands/HSET.spec.ts index eb5fdcd9b32..2cb53e6485a 100644 --- a/packages/client/lib/commands/HSET.spec.ts +++ b/packages/client/lib/commands/HSET.spec.ts @@ -1,27 +1,28 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HSET from './HSET'; +import { parseArgs } from './generic-transformers'; describe('HSET', () => { describe('transformArguments', () => { describe('field, value', () => { it('string', () => { assert.deepEqual( - HSET.transformArguments('key', 'field', 'value'), + parseArgs(HSET, 'key', 'field', 'value'), ['HSET', 'key', 'field', 'value'] ); }); it('number', () => { assert.deepEqual( - HSET.transformArguments('key', 1, 2), + parseArgs(HSET, 'key', 1, 2), ['HSET', 'key', '1', '2'] ); }); it('Buffer', () => { assert.deepEqual( - HSET.transformArguments(Buffer.from('key'), Buffer.from('field'), Buffer.from('value')), + parseArgs(HSET, Buffer.from('key'), Buffer.from('field'), Buffer.from('value')), ['HSET', Buffer.from('key'), Buffer.from('field'), Buffer.from('value')] ); }); @@ -29,14 +30,14 @@ describe('HSET', () => { it('Map', () => { assert.deepEqual( - HSET.transformArguments('key', new Map([['field', 'value']])), + parseArgs(HSET, 'key', new Map([['field', 'value']])), ['HSET', 'key', 'field', 'value'] ); }); it('Array', () => { assert.deepEqual( - HSET.transformArguments('key', [['field', 'value']]), + parseArgs(HSET, 'key', [['field', 'value']]), ['HSET', 'key', 'field', 'value'] ); }); @@ -44,14 +45,14 @@ describe('HSET', () => { describe('Object', () => { it('string', () => { assert.deepEqual( - HSET.transformArguments('key', { field: 'value' }), + parseArgs(HSET, 'key', { field: 'value' }), ['HSET', 'key', 'field', 'value'] ); }); it('Buffer', () => { assert.deepEqual( - HSET.transformArguments('key', { field: Buffer.from('value') }), + parseArgs(HSET, 'key', { field: Buffer.from('value') }), ['HSET', 'key', 'field', Buffer.from('value')] ); }); diff --git a/packages/client/lib/commands/HSET.ts b/packages/client/lib/commands/HSET.ts index e14aa9d06f6..1f50aeacf03 100644 --- a/packages/client/lib/commands/HSET.ts +++ b/packages/client/lib/commands/HSET.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export type HashTypes = RedisArgument | number; @@ -17,51 +18,49 @@ type MultipleFieldsArguments = [...generic: GenericArguments, value: HSETObject export type HSETArguments = SingleFieldArguments | MultipleFieldsArguments; export default { - FIRST_KEY_INDEX: 1, - transformArguments(...[key, value, fieldValue]: SingleFieldArguments | MultipleFieldsArguments) { - const args: Array = ['HSET', key]; + parseCommand(parser: CommandParser, ...[key, value, fieldValue]: SingleFieldArguments | MultipleFieldsArguments) { + parser.push('HSET'); + parser.pushKey(key); if (typeof value === 'string' || typeof value === 'number' || value instanceof Buffer) { - args.push( + parser.push( convertValue(value), convertValue(fieldValue!) ); } else if (value instanceof Map) { - pushMap(args, value); + pushMap(parser, value); } else if (Array.isArray(value)) { - pushTuples(args, value); + pushTuples(parser, value); } else { - pushObject(args, value); + pushObject(parser, value); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; -function pushMap(args: Array, map: HSETMap): void { +function pushMap(parser: CommandParser, map: HSETMap): void { for (const [key, value] of map.entries()) { - args.push( + parser.push( convertValue(key), convertValue(value) ); } } -function pushTuples(args: Array, tuples: HSETTuples): void { +function pushTuples(parser: CommandParser, tuples: HSETTuples): void { for (const tuple of tuples) { if (Array.isArray(tuple)) { - pushTuples(args, tuple); + pushTuples(parser, tuple); continue; } - args.push(convertValue(tuple)); + parser.push(convertValue(tuple)); } } -function pushObject(args: Array, object: HSETObject): void { +function pushObject(parser: CommandParser, object: HSETObject): void { for (const key of Object.keys(object)) { - args.push( + parser.push( convertValue(key), convertValue(object[key]) ); diff --git a/packages/client/lib/commands/HSETNX.spec.ts b/packages/client/lib/commands/HSETNX.spec.ts index 522732624e1..e65f9fb219c 100644 --- a/packages/client/lib/commands/HSETNX.spec.ts +++ b/packages/client/lib/commands/HSETNX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HSETNX from './HSETNX'; +import { parseArgs } from './generic-transformers'; describe('HSETNX', () => { it('transformArguments', () => { assert.deepEqual( - HSETNX.transformArguments('key', 'field', 'value'), + parseArgs(HSETNX, 'key', 'field', 'value'), ['HSETNX', 'key', 'field', 'value'] ); }); diff --git a/packages/client/lib/commands/HSETNX.ts b/packages/client/lib/commands/HSETNX.ts index d26c42a76b4..130d7cd81d3 100644 --- a/packages/client/lib/commands/HSETNX.ts +++ b/packages/client/lib/commands/HSETNX.ts @@ -1,14 +1,17 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, Command, NumberReply } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, field: RedisArgument, value: RedisArgument ) { - return ['HSETNX', key, field, value]; + parser.push('HSETNX'); + parser.pushKey(key); + parser.push(field, value); }, transformReply: undefined as unknown as () => NumberReply<0 | 1> } as const satisfies Command; diff --git a/packages/client/lib/commands/HSTRLEN.spec.ts b/packages/client/lib/commands/HSTRLEN.spec.ts index 59b737b692b..47dd0eaf795 100644 --- a/packages/client/lib/commands/HSTRLEN.spec.ts +++ b/packages/client/lib/commands/HSTRLEN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HSTRLEN from './HSTRLEN'; +import { parseArgs } from './generic-transformers'; describe('HSTRLEN', () => { it('transformArguments', () => { assert.deepEqual( - HSTRLEN.transformArguments('key', 'field'), + parseArgs(HSTRLEN, 'key', 'field'), ['HSTRLEN', 'key', 'field'] ); }); diff --git a/packages/client/lib/commands/HSTRLEN.ts b/packages/client/lib/commands/HSTRLEN.ts index 843c4baf7ef..2468747d4c9 100644 --- a/packages/client/lib/commands/HSTRLEN.ts +++ b/packages/client/lib/commands/HSTRLEN.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, field: RedisArgument) { - return ['HSTRLEN', key, field]; + parseCommand(parser: CommandParser, key: RedisArgument, field: RedisArgument) { + parser.push('HSTRLEN'); + parser.pushKey(key); + parser.push(field); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HTTL.spec.ts b/packages/client/lib/commands/HTTL.spec.ts index df74c8a728e..a79500e4d06 100644 --- a/packages/client/lib/commands/HTTL.spec.ts +++ b/packages/client/lib/commands/HTTL.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HTTL from './HTTL'; import { HASH_EXPIRATION_TIME } from './HEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('HTTL', () => { testUtils.isVersionGreaterThanHook([7, 4]); @@ -9,18 +10,17 @@ describe('HTTL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - HTTL.transformArguments('key', 'field'), + parseArgs(HTTL, 'key', 'field'), ['HTTL', 'key', 'FIELDS', '1', 'field'] ); }); it('array', () => { assert.deepEqual( - HTTL.transformArguments('key', ['field1', 'field2']), + parseArgs(HTTL, 'key', ['field1', 'field2']), ['HTTL', 'key', 'FIELDS', '2', 'field1', 'field2'] ); }); - }); testUtils.testWithClient('hTTL', async client => { diff --git a/packages/client/lib/commands/HTTL.ts b/packages/client/lib/commands/HTTL.ts index 66b50ff3e68..4b8fe5d7e85 100644 --- a/packages/client/lib/commands/HTTL.ts +++ b/packages/client/lib/commands/HTTL.ts @@ -1,11 +1,18 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, Command, NullReply, NumberReply, RedisArgument } from '../RESP/types'; -import { pushVariadicArgument, RedisVariadicArgument } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, fields: RedisVariadicArgument) { - return pushVariadicArgument(['HTTL', key, 'FIELDS'], fields); + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument + ) { + parser.push('HTTL'); + parser.pushKey(key); + parser.push('FIELDS'); + parser.pushVariadicWithLength(fields); }, transformReply: undefined as unknown as () => ArrayReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/HVALS.spec.ts b/packages/client/lib/commands/HVALS.spec.ts index 922aa588137..89cbb52861c 100644 --- a/packages/client/lib/commands/HVALS.spec.ts +++ b/packages/client/lib/commands/HVALS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import HVALS from './HVALS'; +import { parseArgs } from './generic-transformers'; describe('HVALS', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - HVALS.transformArguments('key'), + parseArgs(HVALS, 'key'), ['HVALS', 'key'] ); }); diff --git a/packages/client/lib/commands/HVALS.ts b/packages/client/lib/commands/HVALS.ts index ee4193f5cec..ab17e47f533 100644 --- a/packages/client/lib/commands/HVALS.ts +++ b/packages/client/lib/commands/HVALS.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['HVALS', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('HVALS'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/INCR.spec.ts b/packages/client/lib/commands/INCR.spec.ts index 67129760245..0fe7ed7f8e6 100644 --- a/packages/client/lib/commands/INCR.spec.ts +++ b/packages/client/lib/commands/INCR.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import INCR from './INCR'; +import { parseArgs } from './generic-transformers'; describe('INCR', () => { it('transformArguments', () => { assert.deepEqual( - INCR.transformArguments('key'), + parseArgs(INCR, 'key'), ['INCR', 'key'] ); }); diff --git a/packages/client/lib/commands/INCR.ts b/packages/client/lib/commands/INCR.ts index eb38256c9dc..e719f06bc19 100644 --- a/packages/client/lib/commands/INCR.ts +++ b/packages/client/lib/commands/INCR.ts @@ -1,9 +1,10 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument) { - return ['INCR', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('INCR'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/INCRBY.spec.ts b/packages/client/lib/commands/INCRBY.spec.ts index d66c01acce5..e2a5842f20a 100644 --- a/packages/client/lib/commands/INCRBY.spec.ts +++ b/packages/client/lib/commands/INCRBY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import INCRBY from './INCRBY'; +import { parseArgs } from './generic-transformers'; describe('INCRBY', () => { it('transformArguments', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1), + parseArgs(INCRBY, 'key', 1), ['INCRBY', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/INCRBY.ts b/packages/client/lib/commands/INCRBY.ts index 5e94348fe63..bf463185044 100644 --- a/packages/client/lib/commands/INCRBY.ts +++ b/packages/client/lib/commands/INCRBY.ts @@ -1,9 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, increment: number) { - return ['INCRBY', key, increment.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, increment: number) { + parser.push('INCRBY'); + parser.pushKey(key); + parser.push(increment.toString()); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/INCRBYFLOAT.spec.ts b/packages/client/lib/commands/INCRBYFLOAT.spec.ts index 8bdd9c332de..57596970708 100644 --- a/packages/client/lib/commands/INCRBYFLOAT.spec.ts +++ b/packages/client/lib/commands/INCRBYFLOAT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import INCRBYFLOAT from './INCRBYFLOAT'; +import { parseArgs } from './generic-transformers'; describe('INCRBYFLOAT', () => { it('transformArguments', () => { assert.deepEqual( - INCRBYFLOAT.transformArguments('key', 1.5), + parseArgs(INCRBYFLOAT, 'key', 1.5), ['INCRBYFLOAT', 'key', '1.5'] ); }); diff --git a/packages/client/lib/commands/INCRBYFLOAT.ts b/packages/client/lib/commands/INCRBYFLOAT.ts index 16f59e68c17..9a2dba42a6e 100644 --- a/packages/client/lib/commands/INCRBYFLOAT.ts +++ b/packages/client/lib/commands/INCRBYFLOAT.ts @@ -1,9 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, increment: number) { - return ['INCRBYFLOAT', key, increment.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, increment: number) { + parser.push('INCRBYFLOAT'); + parser.pushKey(key); + parser.push(increment.toString()); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/INFO.spec.ts b/packages/client/lib/commands/INFO.spec.ts index c4555778729..7ee8a95c137 100644 --- a/packages/client/lib/commands/INFO.spec.ts +++ b/packages/client/lib/commands/INFO.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import INFO from './INFO'; +import { parseArgs } from './generic-transformers'; describe('INFO', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - INFO.transformArguments(), + parseArgs(INFO), ['INFO'] ); }); it('server section', () => { assert.deepEqual( - INFO.transformArguments('server'), + parseArgs(INFO, 'server'), ['INFO', 'server'] ); }); diff --git a/packages/client/lib/commands/INFO.ts b/packages/client/lib/commands/INFO.ts index 9877c0cf66b..82cbd497a5b 100644 --- a/packages/client/lib/commands/INFO.ts +++ b/packages/client/lib/commands/INFO.ts @@ -1,16 +1,15 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, VerbatimStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(section?: RedisArgument) { - const args: Array = ['INFO']; + parseCommand(parser: CommandParser, section?: RedisArgument) { + parser.push('INFO'); if (section) { - args.push(section); + parser.push(section); } - - return args; }, transformReply: undefined as unknown as () => VerbatimStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/KEYS.ts b/packages/client/lib/commands/KEYS.ts index 488ba1154c9..e516245d2ee 100644 --- a/packages/client/lib/commands/KEYS.ts +++ b/packages/client/lib/commands/KEYS.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(pattern: RedisArgument) { - return ['KEYS', pattern]; + parseCommand(parser: CommandParser, pattern: RedisArgument) { + parser.push('KEYS', pattern); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LASTSAVE.spec.ts b/packages/client/lib/commands/LASTSAVE.spec.ts index 74cf30705fa..fba26811170 100644 --- a/packages/client/lib/commands/LASTSAVE.spec.ts +++ b/packages/client/lib/commands/LASTSAVE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LASTSAVE from './LASTSAVE'; +import { parseArgs } from './generic-transformers'; describe('LASTSAVE', () => { it('transformArguments', () => { assert.deepEqual( - LASTSAVE.transformArguments(), + parseArgs(LASTSAVE), ['LASTSAVE'] ); }); diff --git a/packages/client/lib/commands/LASTSAVE.ts b/packages/client/lib/commands/LASTSAVE.ts index a65161f78b9..447cb95ab6d 100644 --- a/packages/client/lib/commands/LASTSAVE.ts +++ b/packages/client/lib/commands/LASTSAVE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['LASTSAVE']; + parseCommand(parser: CommandParser) { + parser.push('LASTSAVE'); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts b/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts index 00eabfb4cb3..654751b5b57 100644 --- a/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts +++ b/packages/client/lib/commands/LATENCY_DOCTOR.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LATENCY_DOCTOR from './LATENCY_DOCTOR'; +import { parseArgs } from './generic-transformers'; describe('LATENCY DOCTOR', () => { it('transformArguments', () => { assert.deepEqual( - LATENCY_DOCTOR.transformArguments(), + parseArgs(LATENCY_DOCTOR), ['LATENCY', 'DOCTOR'] ); }); diff --git a/packages/client/lib/commands/LATENCY_DOCTOR.ts b/packages/client/lib/commands/LATENCY_DOCTOR.ts index 96dc6b65702..49c830b3065 100644 --- a/packages/client/lib/commands/LATENCY_DOCTOR.ts +++ b/packages/client/lib/commands/LATENCY_DOCTOR.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['LATENCY', 'DOCTOR']; + parseCommand(parser: CommandParser) { + parser.push('LATENCY', 'DOCTOR'); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LATENCY_GRAPH.spec.ts b/packages/client/lib/commands/LATENCY_GRAPH.spec.ts index 03ad8e2c886..7135dc1c420 100644 --- a/packages/client/lib/commands/LATENCY_GRAPH.spec.ts +++ b/packages/client/lib/commands/LATENCY_GRAPH.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LATENCY_GRAPH from './LATENCY_GRAPH'; +import { parseArgs } from './generic-transformers'; describe('LATENCY GRAPH', () => { it('transformArguments', () => { assert.deepEqual( - LATENCY_GRAPH.transformArguments('command'), + parseArgs(LATENCY_GRAPH, 'command'), [ 'LATENCY', 'GRAPH', diff --git a/packages/client/lib/commands/LATENCY_GRAPH.ts b/packages/client/lib/commands/LATENCY_GRAPH.ts index 7d5f54288b6..20251c3cded 100644 --- a/packages/client/lib/commands/LATENCY_GRAPH.ts +++ b/packages/client/lib/commands/LATENCY_GRAPH.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export const LATENCY_EVENTS = { @@ -22,10 +23,10 @@ export const LATENCY_EVENTS = { export type LatencyEvent = typeof LATENCY_EVENTS[keyof typeof LATENCY_EVENTS]; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(event: LatencyEvent) { - return ['LATENCY', 'GRAPH', event]; + parseCommand(parser: CommandParser, event: LatencyEvent) { + parser.push('LATENCY', 'GRAPH', event); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LATENCY_HISTORY.spec.ts b/packages/client/lib/commands/LATENCY_HISTORY.spec.ts index 509c856e28f..64f94d0d1a3 100644 --- a/packages/client/lib/commands/LATENCY_HISTORY.spec.ts +++ b/packages/client/lib/commands/LATENCY_HISTORY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import LATENCY_HISTORY from './LATENCY_HISTORY'; +import { parseArgs } from './generic-transformers'; describe('LATENCY HISTORY', () => { it('transformArguments', () => { assert.deepEqual( - LATENCY_HISTORY.transformArguments('command'), + parseArgs(LATENCY_HISTORY, 'command'), ['LATENCY', 'HISTORY', 'command'] ); }); diff --git a/packages/client/lib/commands/LATENCY_HISTORY.ts b/packages/client/lib/commands/LATENCY_HISTORY.ts index df5b1772b2d..6e0e4d5c560 100644 --- a/packages/client/lib/commands/LATENCY_HISTORY.ts +++ b/packages/client/lib/commands/LATENCY_HISTORY.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, TuplesReply, NumberReply, Command } from '../RESP/types'; export type LatencyEventType = ( @@ -20,10 +21,10 @@ export type LatencyEventType = ( ); export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(event: LatencyEventType) { - return ['LATENCY', 'HISTORY', event]; + parseCommand(parser: CommandParser, event: LatencyEventType) { + parser.push('LATENCY', 'HISTORY', event); }, transformReply: undefined as unknown as () => ArrayReply { it('transformArguments', () => { assert.deepEqual( - LATENCY_LATEST.transformArguments(), + parseArgs(LATENCY_LATEST), ['LATENCY', 'LATEST'] ); }); diff --git a/packages/client/lib/commands/LATENCY_LATEST.ts b/packages/client/lib/commands/LATENCY_LATEST.ts index 29548af30dc..2ce3efd291c 100644 --- a/packages/client/lib/commands/LATENCY_LATEST.ts +++ b/packages/client/lib/commands/LATENCY_LATEST.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['LATENCY', 'LATEST']; + parseCommand(parser: CommandParser) { + parser.push('LATENCY', 'LATEST'); }, transformReply: undefined as unknown as () => ArrayReply<[ name: BlobStringReply, diff --git a/packages/client/lib/commands/LCS.spec.ts b/packages/client/lib/commands/LCS.spec.ts index ff9d63db1b1..aedbb1b34e3 100644 --- a/packages/client/lib/commands/LCS.spec.ts +++ b/packages/client/lib/commands/LCS.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LCS from './LCS'; +import { parseArgs } from './generic-transformers'; describe('LCS', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - LCS.transformArguments('1', '2'), + parseArgs(LCS, '1', '2'), ['LCS', '1', '2'] ); }); diff --git a/packages/client/lib/commands/LCS.ts b/packages/client/lib/commands/LCS.ts index b798f12aa37..ed4f11ad990 100644 --- a/packages/client/lib/commands/LCS.ts +++ b/packages/client/lib/commands/LCS.ts @@ -1,13 +1,15 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key1: RedisArgument, key2: RedisArgument ) { - return ['LCS', key1, key2]; + parser.push('LCS'); + parser.pushKeys([key1, key2]); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LCS_IDX.spec.ts b/packages/client/lib/commands/LCS_IDX.spec.ts index 2f60a205faa..c4cc6681d85 100644 --- a/packages/client/lib/commands/LCS_IDX.spec.ts +++ b/packages/client/lib/commands/LCS_IDX.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LCS_IDX from './LCS_IDX'; +import { parseArgs } from './generic-transformers'; describe('LCS IDX', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - LCS_IDX.transformArguments('1', '2'), + parseArgs(LCS_IDX, '1', '2'), ['LCS', '1', '2', 'IDX'] ); }); diff --git a/packages/client/lib/commands/LCS_IDX.ts b/packages/client/lib/commands/LCS_IDX.ts index 0c266fffe1c..cb0a6b07657 100644 --- a/packages/client/lib/commands/LCS_IDX.ts +++ b/packages/client/lib/commands/LCS_IDX.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, NumberReply, UnwrapReply, Resp2Reply, Command, TuplesReply } from '../RESP/types'; import LCS from './LCS'; @@ -23,22 +24,20 @@ export type LcsIdxReply = TuplesToMapReply<[ ]>; export default { - FIRST_KEY_INDEX: LCS.FIRST_KEY_INDEX, IS_READ_ONLY: LCS.IS_READ_ONLY, - transformArguments( + parseCommand( + parser: CommandParser, key1: RedisArgument, key2: RedisArgument, options?: LcsIdxOptions ) { - const args = LCS.transformArguments(key1, key2); + LCS.parseCommand(parser, key1, key2); - args.push('IDX'); + parser.push('IDX'); if (options?.MINMATCHLEN) { - args.push('MINMATCHLEN', options.MINMATCHLEN.toString()); + parser.push('MINMATCHLEN', options.MINMATCHLEN.toString()); } - - return args; }, transformReply: { 2: (reply: UnwrapReply>) => ({ diff --git a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts index 39ba17e8f2c..92ecad4761c 100644 --- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts +++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LCS_IDX_WITHMATCHLEN from './LCS_IDX_WITHMATCHLEN'; +import { parseArgs } from './generic-transformers'; describe('LCS IDX WITHMATCHLEN', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - LCS_IDX_WITHMATCHLEN.transformArguments('1', '2'), + parseArgs(LCS_IDX_WITHMATCHLEN, '1', '2'), ['LCS', '1', '2', 'IDX', 'WITHMATCHLEN'] ); }); diff --git a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts index 4e645852035..d2a743983e1 100644 --- a/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts +++ b/packages/client/lib/commands/LCS_IDX_WITHMATCHLEN.ts @@ -1,5 +1,5 @@ -import { RedisArgument, TuplesToMapReply, BlobStringReply, ArrayReply, TuplesReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; -import LCS_IDX, { LcsIdxOptions, LcsIdxRange } from './LCS_IDX'; +import { TuplesToMapReply, BlobStringReply, ArrayReply, TuplesReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; +import LCS_IDX, { LcsIdxRange } from './LCS_IDX'; export type LcsIdxWithMatchLenMatches = ArrayReply< TuplesReply<[ @@ -15,16 +15,11 @@ export type LcsIdxWithMatchLenReply = TuplesToMapReply<[ ]>; export default { - FIRST_KEY_INDEX: LCS_IDX.FIRST_KEY_INDEX, IS_READ_ONLY: LCS_IDX.IS_READ_ONLY, - transformArguments( - key1: RedisArgument, - key2: RedisArgument, - options?: LcsIdxOptions - ) { - const args = LCS_IDX.transformArguments(key1, key2); - args.push('WITHMATCHLEN'); - return args; + parseCommand(...args: Parameters) { + const parser = args[0]; + LCS_IDX.parseCommand(...args); + parser.push('WITHMATCHLEN'); }, transformReply: { 2: (reply: UnwrapReply>) => ({ diff --git a/packages/client/lib/commands/LCS_LEN.spec.ts b/packages/client/lib/commands/LCS_LEN.spec.ts index 9dc163a68a9..53a2e83c326 100644 --- a/packages/client/lib/commands/LCS_LEN.spec.ts +++ b/packages/client/lib/commands/LCS_LEN.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LCS_LEN from './LCS_LEN'; +import { parseArgs } from './generic-transformers'; describe('LCS_LEN', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - LCS_LEN.transformArguments('1', '2'), + parseArgs(LCS_LEN, '1', '2'), ['LCS', '1', '2', 'LEN'] ); }); diff --git a/packages/client/lib/commands/LCS_LEN.ts b/packages/client/lib/commands/LCS_LEN.ts index d5d0e77e4d6..a1f92d914a4 100644 --- a/packages/client/lib/commands/LCS_LEN.ts +++ b/packages/client/lib/commands/LCS_LEN.ts @@ -1,16 +1,13 @@ -import { RedisArgument, NumberReply, Command } from '../RESP/types'; +import { NumberReply, Command } from '../RESP/types'; import LCS from './LCS'; export default { - FIRST_KEY_INDEX: LCS.FIRST_KEY_INDEX, IS_READ_ONLY: LCS.IS_READ_ONLY, - transformArguments( - key1: RedisArgument, - key2: RedisArgument - ) { - const args = LCS.transformArguments(key1, key2); - args.push('LEN'); - return args; + parseCommand(...args: Parameters) { + const parser = args[0]; + + LCS.parseCommand(...args); + parser.push('LEN'); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LINDEX.spec.ts b/packages/client/lib/commands/LINDEX.spec.ts index 60346b85f21..41eff474a1a 100644 --- a/packages/client/lib/commands/LINDEX.spec.ts +++ b/packages/client/lib/commands/LINDEX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LINDEX from './LINDEX'; +import { parseArgs } from './generic-transformers'; describe('LINDEX', () => { it('transformArguments', () => { assert.deepEqual( - LINDEX.transformArguments('key', 0), + parseArgs(LINDEX, 'key', 0), ['LINDEX', 'key', '0'] ); }); diff --git a/packages/client/lib/commands/LINDEX.ts b/packages/client/lib/commands/LINDEX.ts index 0478bf9dc41..6335fc40c2c 100644 --- a/packages/client/lib/commands/LINDEX.ts +++ b/packages/client/lib/commands/LINDEX.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, index: number) { - return ['LINDEX', key, index.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, index: number) { + parser.push('LINDEX'); + parser.pushKey(key); + parser.push(index.toString()); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LINSERT.spec.ts b/packages/client/lib/commands/LINSERT.spec.ts index 6cafa3f5a84..c3c89d56c12 100644 --- a/packages/client/lib/commands/LINSERT.spec.ts +++ b/packages/client/lib/commands/LINSERT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LINSERT from './LINSERT'; +import { parseArgs } from './generic-transformers'; describe('LINSERT', () => { it('transformArguments', () => { assert.deepEqual( - LINSERT.transformArguments('key', 'BEFORE', 'pivot', 'element'), + parseArgs(LINSERT, 'key', 'BEFORE', 'pivot', 'element'), ['LINSERT', 'key', 'BEFORE', 'pivot', 'element'] ); }); diff --git a/packages/client/lib/commands/LINSERT.ts b/packages/client/lib/commands/LINSERT.ts index 4bdc77de5a4..8a40ac66630 100644 --- a/packages/client/lib/commands/LINSERT.ts +++ b/packages/client/lib/commands/LINSERT.ts @@ -1,23 +1,20 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; type LInsertPosition = 'BEFORE' | 'AFTER'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, position: LInsertPosition, pivot: RedisArgument, element: RedisArgument ) { - return [ - 'LINSERT', - key, - position, - pivot, - element - ]; + parser.push('LINSERT'); + parser.pushKey(key); + parser.push(position, pivot, element); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LLEN.spec.ts b/packages/client/lib/commands/LLEN.spec.ts index f6ac9a73cc3..d86078d0b48 100644 --- a/packages/client/lib/commands/LLEN.spec.ts +++ b/packages/client/lib/commands/LLEN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LLEN from './LLEN'; +import { parseArgs } from './generic-transformers'; describe('LLEN', () => { it('transformArguments', () => { assert.deepEqual( - LLEN.transformArguments('key'), + parseArgs(LLEN, 'key'), ['LLEN', 'key'] ); }); diff --git a/packages/client/lib/commands/LLEN.ts b/packages/client/lib/commands/LLEN.ts index dda59ddf291..674e022e60d 100644 --- a/packages/client/lib/commands/LLEN.ts +++ b/packages/client/lib/commands/LLEN.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['LLEN', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('LLEN'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LMOVE.spec.ts b/packages/client/lib/commands/LMOVE.spec.ts index 86740aa7b77..bed3ff8eab0 100644 --- a/packages/client/lib/commands/LMOVE.spec.ts +++ b/packages/client/lib/commands/LMOVE.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LMOVE from './LMOVE'; +import { parseArgs } from './generic-transformers'; describe('LMOVE', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - LMOVE.transformArguments('source', 'destination', 'LEFT', 'RIGHT'), + parseArgs(LMOVE, 'source', 'destination', 'LEFT', 'RIGHT'), ['LMOVE', 'source', 'destination', 'LEFT', 'RIGHT'] ); }); diff --git a/packages/client/lib/commands/LMOVE.ts b/packages/client/lib/commands/LMOVE.ts index 95adc71a0ff..f3ac847e900 100644 --- a/packages/client/lib/commands/LMOVE.ts +++ b/packages/client/lib/commands/LMOVE.ts @@ -1,22 +1,19 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; import { ListSide } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, source: RedisArgument, destination: RedisArgument, sourceSide: ListSide, destinationSide: ListSide ) { - return [ - 'LMOVE', - source, - destination, - sourceSide, - destinationSide, - ]; + parser.push('LMOVE'); + parser.pushKeys([source, destination]); + parser.push(sourceSide, destinationSide); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LMPOP.spec.ts b/packages/client/lib/commands/LMPOP.spec.ts index faf39e053ef..bd2cf869e74 100644 --- a/packages/client/lib/commands/LMPOP.spec.ts +++ b/packages/client/lib/commands/LMPOP.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LMPOP from './LMPOP'; +import { parseArgs } from './generic-transformers'; describe('LMPOP', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,14 +9,14 @@ describe('LMPOP', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - LMPOP.transformArguments('key', 'LEFT'), + parseArgs(LMPOP, 'key', 'LEFT'), ['LMPOP', '1', 'key', 'LEFT'] ); }); it('with COUNT', () => { assert.deepEqual( - LMPOP.transformArguments('key', 'LEFT', { + parseArgs(LMPOP, 'key', 'LEFT', { COUNT: 2 }), ['LMPOP', '1', 'key', 'LEFT', 'COUNT', '2'] diff --git a/packages/client/lib/commands/LMPOP.ts b/packages/client/lib/commands/LMPOP.ts index 49f7272ec46..c8095e42e75 100644 --- a/packages/client/lib/commands/LMPOP.ts +++ b/packages/client/lib/commands/LMPOP.ts @@ -1,34 +1,32 @@ -import { CommandArguments, NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types'; -import { ListSide, RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NullReply, TuplesReply, BlobStringReply, Command } from '../RESP/types'; +import { ListSide, RedisVariadicArgument, Tail } from './generic-transformers'; export interface LMPopOptions { COUNT?: number; } -export function transformLMPopArguments( - args: CommandArguments, +export function parseLMPopArguments( + parser: CommandParser, keys: RedisVariadicArgument, side: ListSide, options?: LMPopOptions -): CommandArguments { - args = pushVariadicArgument(args, keys); - - args.push(side); +) { + parser.pushKeysLength(keys); + parser.push(side); if (options?.COUNT !== undefined) { - args.push('COUNT', options.COUNT.toString()); + parser.push('COUNT', options.COUNT.toString()); } - - return args; } -export type LMPopArguments = typeof transformLMPopArguments extends (_: any, ...args: infer T) => any ? T : never; +export type LMPopArguments = Tail>; export default { - FIRST_KEY_INDEX: 2, - IS_READ_ONLY: false, - transformArguments(...args: LMPopArguments) { - return transformLMPopArguments(['LMPOP'], ...args); + IS_READ_ONLY: false, + parseCommand(parser: CommandParser, ...args: LMPopArguments) { + parser.push('LMPOP'); + parseLMPopArguments(parser, ...args); }, transformReply: undefined as unknown as () => NullReply | TuplesReply<[ key: BlobStringReply, diff --git a/packages/client/lib/commands/LOLWUT.spec.ts b/packages/client/lib/commands/LOLWUT.spec.ts index b05c4168f6f..b06030b0d0e 100644 --- a/packages/client/lib/commands/LOLWUT.spec.ts +++ b/packages/client/lib/commands/LOLWUT.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LOLWUT from './LOLWUT'; +import { parseArgs } from './generic-transformers'; describe('LOLWUT', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - LOLWUT.transformArguments(), + parseArgs(LOLWUT), ['LOLWUT'] ); }); it('with version', () => { assert.deepEqual( - LOLWUT.transformArguments(5), + parseArgs(LOLWUT, 5), ['LOLWUT', 'VERSION', '5'] ); }); it('with version and optional arguments', () => { assert.deepEqual( - LOLWUT.transformArguments(5, 1, 2, 3), + parseArgs(LOLWUT, 5, 1, 2, 3), ['LOLWUT', 'VERSION', '5', '1', '2', '3'] ); }); diff --git a/packages/client/lib/commands/LOLWUT.ts b/packages/client/lib/commands/LOLWUT.ts index 7a6c8329d69..372bf536967 100644 --- a/packages/client/lib/commands/LOLWUT.ts +++ b/packages/client/lib/commands/LOLWUT.ts @@ -1,20 +1,18 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(version?: number, ...optionalArguments: Array) { - const args = ['LOLWUT']; - + parseCommand(parser: CommandParser, version?: number, ...optionalArguments: Array) { + parser.push('LOLWUT'); if (version) { - args.push( + parser.push( 'VERSION', - version.toString(), - ...optionalArguments.map(String), + version.toString() ); + parser.pushVariadic(optionalArguments.map(String)); } - - return args; }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LPOP.spec.ts b/packages/client/lib/commands/LPOP.spec.ts index 944e559b15f..93449bdbf5f 100644 --- a/packages/client/lib/commands/LPOP.spec.ts +++ b/packages/client/lib/commands/LPOP.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LPOP from './LPOP'; +import { parseArgs } from './generic-transformers'; describe('LPOP', () => { it('transformArguments', () => { assert.deepEqual( - LPOP.transformArguments('key'), + parseArgs(LPOP, 'key'), ['LPOP', 'key'] ); }); diff --git a/packages/client/lib/commands/LPOP.ts b/packages/client/lib/commands/LPOP.ts index 7c85c30f9a1..3125236bfa0 100644 --- a/packages/client/lib/commands/LPOP.ts +++ b/packages/client/lib/commands/LPOP.ts @@ -1,9 +1,10 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument) { - return ['LPOP', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('LPOP'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LPOP_COUNT.spec.ts b/packages/client/lib/commands/LPOP_COUNT.spec.ts index 8286a504428..04bb3648d0a 100644 --- a/packages/client/lib/commands/LPOP_COUNT.spec.ts +++ b/packages/client/lib/commands/LPOP_COUNT.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LPOP_COUNT from './LPOP_COUNT'; +import { parseArgs } from './generic-transformers'; describe('LPOP COUNT', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - LPOP_COUNT.transformArguments('key', 1), + parseArgs(LPOP_COUNT, 'key', 1), ['LPOP', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/LPOP_COUNT.ts b/packages/client/lib/commands/LPOP_COUNT.ts index a1536e78dcb..6d9aba42c21 100644 --- a/packages/client/lib/commands/LPOP_COUNT.ts +++ b/packages/client/lib/commands/LPOP_COUNT.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NullReply, ArrayReply, BlobStringReply, Command } from '../RESP/types'; +import LPOP from './LPOP'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, count: number) { - return ['LPOP', key, count.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + LPOP.parseCommand(parser, key); + parser.push(count.toString()) }, transformReply: undefined as unknown as () => NullReply | ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LPOS.spec.ts b/packages/client/lib/commands/LPOS.spec.ts index 94c0bb3c034..f26af3f540f 100644 --- a/packages/client/lib/commands/LPOS.spec.ts +++ b/packages/client/lib/commands/LPOS.spec.ts @@ -1,21 +1,22 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LPOS from './LPOS'; +import { parseArgs } from './generic-transformers'; describe('LPOS', () => { testUtils.isVersionGreaterThanHook([6, 0, 6]); - describe('transformArguments', () => { + describe('processCommand', () => { it('simple', () => { assert.deepEqual( - LPOS.transformArguments('key', 'element'), + parseArgs(LPOS, 'key', 'element'), ['LPOS', 'key', 'element'] ); }); it('with RANK', () => { assert.deepEqual( - LPOS.transformArguments('key', 'element', { + parseArgs(LPOS, 'key', 'element', { RANK: 0 }), ['LPOS', 'key', 'element', 'RANK', '0'] @@ -24,7 +25,7 @@ describe('LPOS', () => { it('with MAXLEN', () => { assert.deepEqual( - LPOS.transformArguments('key', 'element', { + parseArgs(LPOS, 'key', 'element', { MAXLEN: 10 }), ['LPOS', 'key', 'element', 'MAXLEN', '10'] @@ -33,7 +34,7 @@ describe('LPOS', () => { it('with RANK, MAXLEN', () => { assert.deepEqual( - LPOS.transformArguments('key', 'element', { + parseArgs(LPOS, 'key', 'element', { RANK: 0, MAXLEN: 10 }), diff --git a/packages/client/lib/commands/LPOS.ts b/packages/client/lib/commands/LPOS.ts index 047d80d7c29..bb05ba6555d 100644 --- a/packages/client/lib/commands/LPOS.ts +++ b/packages/client/lib/commands/LPOS.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export interface LPosOptions { @@ -6,26 +7,25 @@ export interface LPosOptions { } export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, element: RedisArgument, options?: LPosOptions ) { - const args = ['LPOS', key, element]; + parser.push('LPOS'); + parser.pushKey(key); + parser.push(element); - if (options) { - if (typeof options.RANK === 'number') { - args.push('RANK', options.RANK.toString()); - } - - if (typeof options.MAXLEN === 'number') { - args.push('MAXLEN', options.MAXLEN.toString()); - } + if (options?.RANK !== undefined) { + parser.push('RANK', options.RANK.toString()); } - return args; + if (options?.MAXLEN !== undefined) { + parser.push('MAXLEN', options.MAXLEN.toString()); + } }, transformReply: undefined as unknown as () => NumberReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LPOS_COUNT.spec.ts b/packages/client/lib/commands/LPOS_COUNT.spec.ts index 747ffbc01cf..702ef5a746b 100644 --- a/packages/client/lib/commands/LPOS_COUNT.spec.ts +++ b/packages/client/lib/commands/LPOS_COUNT.spec.ts @@ -1,21 +1,22 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LPOS_COUNT from './LPOS_COUNT'; +import { parseArgs } from './generic-transformers'; describe('LPOS COUNT', () => { testUtils.isVersionGreaterThanHook([6, 0, 6]); - describe('transformArguments', () => { + describe('processCommand', () => { it('simple', () => { assert.deepEqual( - LPOS_COUNT.transformArguments('key', 'element', 0), + parseArgs(LPOS_COUNT, 'key', 'element', 0), ['LPOS', 'key', 'element', 'COUNT', '0'] ); }); it('with RANK', () => { assert.deepEqual( - LPOS_COUNT.transformArguments('key', 'element', 0, { + parseArgs(LPOS_COUNT, 'key', 'element', 0, { RANK: 0 }), ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0'] @@ -24,20 +25,20 @@ describe('LPOS COUNT', () => { it('with MAXLEN', () => { assert.deepEqual( - LPOS_COUNT.transformArguments('key', 'element', 0, { + parseArgs(LPOS_COUNT, 'key', 'element', 0, { MAXLEN: 10 }), - ['LPOS', 'key', 'element', 'COUNT', '0', 'MAXLEN', '10'] + ['LPOS', 'key', 'element', 'MAXLEN', '10', 'COUNT', '0'] ); }); it('with RANK, MAXLEN', () => { assert.deepEqual( - LPOS_COUNT.transformArguments('key', 'element', 0, { + parseArgs(LPOS_COUNT, 'key', 'element', 0, { RANK: 0, MAXLEN: 10 }), - ['LPOS', 'key', 'element', 'RANK', '0', 'COUNT', '0', 'MAXLEN', '10'] + ['LPOS', 'key', 'element', 'RANK', '0', 'MAXLEN', '10', 'COUNT', '0'] ); }); }); diff --git a/packages/client/lib/commands/LPOS_COUNT.ts b/packages/client/lib/commands/LPOS_COUNT.ts index 1b057cff1f6..e782a2d26ee 100644 --- a/packages/client/lib/commands/LPOS_COUNT.ts +++ b/packages/client/lib/commands/LPOS_COUNT.ts @@ -1,28 +1,20 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; import LPOS, { LPosOptions } from './LPOS'; export default { - FIRST_KEY_INDEX: LPOS.FIRST_KEY_INDEX, + CACHEABLE: LPOS.CACHEABLE, IS_READ_ONLY: LPOS.IS_READ_ONLY, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, element: RedisArgument, count: number, options?: LPosOptions ) { - const args = ['LPOS', key, element]; + LPOS.parseCommand(parser, key, element, options); - if (options?.RANK !== undefined) { - args.push('RANK', options.RANK.toString()); - } - - args.push('COUNT', count.toString()); - - if (options?.MAXLEN !== undefined) { - args.push('MAXLEN', options.MAXLEN.toString()); - } - - return args; + parser.push('COUNT', count.toString()); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LPUSH.spec.ts b/packages/client/lib/commands/LPUSH.spec.ts index 8d2ddb5ccc2..09c7d9da772 100644 --- a/packages/client/lib/commands/LPUSH.spec.ts +++ b/packages/client/lib/commands/LPUSH.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LPUSH from './LPUSH'; +import { parseArgs } from './generic-transformers'; describe('LPUSH', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - LPUSH.transformArguments('key', 'field'), + parseArgs(LPUSH, 'key', 'field'), ['LPUSH', 'key', 'field'] ); }); it('array', () => { assert.deepEqual( - LPUSH.transformArguments('key', ['1', '2']), + parseArgs(LPUSH, 'key', ['1', '2']), ['LPUSH', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/LPUSH.ts b/packages/client/lib/commands/LPUSH.ts index 714db2ae9a9..293029034ee 100644 --- a/packages/client/lib/commands/LPUSH.ts +++ b/packages/client/lib/commands/LPUSH.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, elements: RedisVariadicArgument) { - return pushVariadicArguments(['LPUSH', key], elements); + parseCommand(parser: CommandParser, key: RedisArgument, elements: RedisVariadicArgument) { + parser.push('LPUSH'); + parser.pushKey(key); + parser.pushVariadic(elements); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LPUSHX.spec.ts b/packages/client/lib/commands/LPUSHX.spec.ts index f7dafe025c7..179a0ddb29e 100644 --- a/packages/client/lib/commands/LPUSHX.spec.ts +++ b/packages/client/lib/commands/LPUSHX.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LPUSHX from './LPUSHX'; +import { parseArgs } from './generic-transformers'; describe('LPUSHX', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - LPUSHX.transformArguments('key', 'element'), + parseArgs(LPUSHX, 'key', 'element'), ['LPUSHX', 'key', 'element'] ); }); it('array', () => { assert.deepEqual( - LPUSHX.transformArguments('key', ['1', '2']), + parseArgs(LPUSHX, 'key', ['1', '2']), ['LPUSHX', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/LPUSHX.ts b/packages/client/lib/commands/LPUSHX.ts index d6dceda6d02..98dd51a7ac2 100644 --- a/packages/client/lib/commands/LPUSHX.ts +++ b/packages/client/lib/commands/LPUSHX.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, elements: RedisVariadicArgument) { - return pushVariadicArguments(['LPUSHX', key], elements); + parseCommand(parser: CommandParser, key: RedisArgument, elements: RedisVariadicArgument) { + parser.push('LPUSHX'); + parser.pushKey(key); + parser.pushVariadic(elements); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LRANGE.spec.ts b/packages/client/lib/commands/LRANGE.spec.ts index 3e768cc0731..c0bb046d898 100644 --- a/packages/client/lib/commands/LRANGE.spec.ts +++ b/packages/client/lib/commands/LRANGE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LRANGE from './LRANGE'; +import { parseArgs } from './generic-transformers'; describe('LRANGE', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - LRANGE.transformArguments('key', 0, -1), + parseArgs(LRANGE, 'key', 0, -1), ['LRANGE', 'key', '0', '-1'] ); }); diff --git a/packages/client/lib/commands/LRANGE.ts b/packages/client/lib/commands/LRANGE.ts index 4b4b3488baa..ab033dd88a4 100644 --- a/packages/client/lib/commands/LRANGE.ts +++ b/packages/client/lib/commands/LRANGE.ts @@ -1,19 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( - key: RedisArgument, - start: number, - stop: number -) { - return [ - 'LRANGE', - key, - start.toString(), - stop.toString() - ]; + parseCommand(parser: CommandParser, key: RedisArgument, start: number, stop: number) { + parser.push('LRANGE'); + parser.pushKey(key); + parser.push(start.toString(), stop.toString()) }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LREM.spec.ts b/packages/client/lib/commands/LREM.spec.ts index 1a35dab7c54..2a36d8ee2f1 100644 --- a/packages/client/lib/commands/LREM.spec.ts +++ b/packages/client/lib/commands/LREM.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LREM from './LREM'; +import { parseArgs } from './generic-transformers'; describe('LREM', () => { it('transformArguments', () => { assert.deepEqual( - LREM.transformArguments('key', 0, 'element'), + parseArgs(LREM, 'key', 0, 'element'), ['LREM', 'key', '0', 'element'] ); }); diff --git a/packages/client/lib/commands/LREM.ts b/packages/client/lib/commands/LREM.ts index dbd2002be8b..bb97e3882e7 100644 --- a/packages/client/lib/commands/LREM.ts +++ b/packages/client/lib/commands/LREM.ts @@ -1,19 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( - key: RedisArgument, - count: number, - element: RedisArgument - ) { - return [ - 'LREM', - key, - count.toString(), - element - ]; + parseCommand(parser: CommandParser, key: RedisArgument, count: number, element: RedisArgument) { + parser.push('LREM'); + parser.pushKey(key); + parser.push(count.toString()); + parser.push(element); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/LSET.spec.ts b/packages/client/lib/commands/LSET.spec.ts index ae4b1bf9f2f..c7522942402 100644 --- a/packages/client/lib/commands/LSET.spec.ts +++ b/packages/client/lib/commands/LSET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LSET from './LSET'; +import { parseArgs } from './generic-transformers'; describe('LSET', () => { it('transformArguments', () => { assert.deepEqual( - LSET.transformArguments('key', 0, 'element'), + parseArgs(LSET, 'key', 0, 'element'), ['LSET', 'key', '0', 'element'] ); }); diff --git a/packages/client/lib/commands/LSET.ts b/packages/client/lib/commands/LSET.ts index edb86baae0f..0fe646fbb73 100644 --- a/packages/client/lib/commands/LSET.ts +++ b/packages/client/lib/commands/LSET.ts @@ -1,19 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( - key: RedisArgument, - index: number, - element: RedisArgument - ) { - return [ - 'LSET', - key, - index.toString(), - element - ]; + parseCommand(parser: CommandParser, key: RedisArgument, index: number, element: RedisArgument) { + parser.push('LSET'); + parser.pushKey(key); + parser.push(index.toString(), element); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/LTRIM.spec.ts b/packages/client/lib/commands/LTRIM.spec.ts index 3edc3669737..5b6d77c91de 100644 --- a/packages/client/lib/commands/LTRIM.spec.ts +++ b/packages/client/lib/commands/LTRIM.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LTRIM from './LTRIM'; +import { parseArgs } from './generic-transformers'; describe('LTRIM', () => { it('transformArguments', () => { assert.deepEqual( - LTRIM.transformArguments('key', 0, -1), + parseArgs(LTRIM, 'key', 0, -1), ['LTRIM', 'key', '0', '-1'] ); }); diff --git a/packages/client/lib/commands/LTRIM.ts b/packages/client/lib/commands/LTRIM.ts index b80aa6264e9..acc7e767d0d 100644 --- a/packages/client/lib/commands/LTRIM.ts +++ b/packages/client/lib/commands/LTRIM.ts @@ -1,18 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( - key: RedisArgument, - start: number, - stop: number - ) { - return [ - 'LTRIM', - key, - start.toString(), - stop.toString() - ]; + parseCommand(parser: CommandParser, key: RedisArgument, start: number, stop: number) { + parser.push('LTRIM'); + parser.pushKey(key); + parser.push(start.toString(), stop.toString()); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts b/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts index e1d718d7aab..9d822f8e07e 100644 --- a/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts +++ b/packages/client/lib/commands/MEMORY_DOCTOR.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MEMORY_DOCTOR from './MEMORY_DOCTOR'; +import { parseArgs } from './generic-transformers'; describe('MEMORY DOCTOR', () => { it('transformArguments', () => { assert.deepEqual( - MEMORY_DOCTOR.transformArguments(), + parseArgs(MEMORY_DOCTOR), ['MEMORY', 'DOCTOR'] ); }); diff --git a/packages/client/lib/commands/MEMORY_DOCTOR.ts b/packages/client/lib/commands/MEMORY_DOCTOR.ts index 6f8eb29ef2b..3a2d808db10 100644 --- a/packages/client/lib/commands/MEMORY_DOCTOR.ts +++ b/packages/client/lib/commands/MEMORY_DOCTOR.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['MEMORY', 'DOCTOR']; + parseCommand(parser: CommandParser) { + parser.push('MEMORY', 'DOCTOR'); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts b/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts index 8484fcbf0b7..a4a85f5b994 100644 --- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts +++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MEMORY_MALLOC_STATS from './MEMORY_MALLOC-STATS'; +import { parseArgs } from './generic-transformers'; describe('MEMORY MALLOC-STATS', () => { it('transformArguments', () => { assert.deepEqual( - MEMORY_MALLOC_STATS.transformArguments(), + parseArgs(MEMORY_MALLOC_STATS), ['MEMORY', 'MALLOC-STATS'] ); }); diff --git a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts index 31b01a49b5c..af6b5db3347 100644 --- a/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts +++ b/packages/client/lib/commands/MEMORY_MALLOC-STATS.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['MEMORY', 'MALLOC-STATS']; + parseCommand(parser: CommandParser) { + parser.push('MEMORY', 'MALLOC-STATS'); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/MEMORY_PURGE.spec.ts b/packages/client/lib/commands/MEMORY_PURGE.spec.ts index 41b01f87c4b..be5fb738b0a 100644 --- a/packages/client/lib/commands/MEMORY_PURGE.spec.ts +++ b/packages/client/lib/commands/MEMORY_PURGE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MEMORY_PURGE from './MEMORY_PURGE'; +import { parseArgs } from './generic-transformers'; describe('MEMORY PURGE', () => { it('transformArguments', () => { assert.deepEqual( - MEMORY_PURGE.transformArguments(), + parseArgs(MEMORY_PURGE), ['MEMORY', 'PURGE'] ); }); diff --git a/packages/client/lib/commands/MEMORY_PURGE.ts b/packages/client/lib/commands/MEMORY_PURGE.ts index b4c48d40102..bbd02890786 100644 --- a/packages/client/lib/commands/MEMORY_PURGE.ts +++ b/packages/client/lib/commands/MEMORY_PURGE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments() { - return ['MEMORY', 'PURGE']; + parseCommand(parser: CommandParser) { + parser.push('MEMORY', 'PURGE'); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/MEMORY_STATS.spec.ts b/packages/client/lib/commands/MEMORY_STATS.spec.ts index 6d5f5b8690b..6aad05116af 100644 --- a/packages/client/lib/commands/MEMORY_STATS.spec.ts +++ b/packages/client/lib/commands/MEMORY_STATS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MEMORY_STATS from './MEMORY_STATS'; +import { parseArgs } from './generic-transformers'; describe('MEMORY STATS', () => { it('transformArguments', () => { assert.deepEqual( - MEMORY_STATS.transformArguments(), + parseArgs(MEMORY_STATS), ['MEMORY', 'STATS'] ); }); diff --git a/packages/client/lib/commands/MEMORY_STATS.ts b/packages/client/lib/commands/MEMORY_STATS.ts index f38a0e5f29b..33410535aa9 100644 --- a/packages/client/lib/commands/MEMORY_STATS.ts +++ b/packages/client/lib/commands/MEMORY_STATS.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { TuplesToMapReply, BlobStringReply, NumberReply, DoubleReply, ArrayReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; import { transformDoubleReply } from './generic-transformers'; @@ -35,10 +36,10 @@ export type MemoryStatsReply = TuplesToMapReply<[ ]>; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['MEMORY', 'STATS']; + parseCommand(parser: CommandParser) { + parser.push('MEMORY', 'STATS'); }, transformReply: { 2: (rawReply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { diff --git a/packages/client/lib/commands/MEMORY_USAGE.spec.ts b/packages/client/lib/commands/MEMORY_USAGE.spec.ts index 250a6884259..edf673564ee 100644 --- a/packages/client/lib/commands/MEMORY_USAGE.spec.ts +++ b/packages/client/lib/commands/MEMORY_USAGE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MEMORY_USAGE from './MEMORY_USAGE'; +import { parseArgs } from './generic-transformers'; describe('MEMORY USAGE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - MEMORY_USAGE.transformArguments('key'), + parseArgs(MEMORY_USAGE, 'key'), ['MEMORY', 'USAGE', 'key'] ); }); it('with SAMPLES', () => { assert.deepEqual( - MEMORY_USAGE.transformArguments('key', { + parseArgs(MEMORY_USAGE, 'key', { SAMPLES: 1 }), ['MEMORY', 'USAGE', 'key', 'SAMPLES', '1'] diff --git a/packages/client/lib/commands/MEMORY_USAGE.ts b/packages/client/lib/commands/MEMORY_USAGE.ts index 78b079c2c13..6e85438dbed 100644 --- a/packages/client/lib/commands/MEMORY_USAGE.ts +++ b/packages/client/lib/commands/MEMORY_USAGE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, NullReply, Command, RedisArgument } from '../RESP/types'; export interface MemoryUsageOptions { @@ -5,16 +6,14 @@ export interface MemoryUsageOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, options?: MemoryUsageOptions) { - const args = ['MEMORY', 'USAGE', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: MemoryUsageOptions) { + parser.push('MEMORY', 'USAGE'); + parser.pushKey(key); if (options?.SAMPLES) { - args.push('SAMPLES', options.SAMPLES.toString()); + parser.push('SAMPLES', options.SAMPLES.toString()); } - - return args; }, transformReply: undefined as unknown as () => NumberReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/MGET.spec.ts b/packages/client/lib/commands/MGET.spec.ts index 296702a1a03..048fa6f0a58 100644 --- a/packages/client/lib/commands/MGET.spec.ts +++ b/packages/client/lib/commands/MGET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MGET from './MGET'; +import { parseArgs } from './generic-transformers'; describe('MGET', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - MGET.transformArguments(['1', '2']), + parseArgs(MGET, ['1', '2']), ['MGET', '1', '2'] ); }); diff --git a/packages/client/lib/commands/MGET.ts b/packages/client/lib/commands/MGET.ts index 0f0f9e52ffb..ce1e9ba7781 100644 --- a/packages/client/lib/commands/MGET.ts +++ b/packages/client/lib/commands/MGET.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(keys: Array) { - return ['MGET', ...keys]; + parseCommand(parser: CommandParser, keys: Array) { + parser.push('MGET'); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => Array } as const satisfies Command; diff --git a/packages/client/lib/commands/MIGRATE.spec.ts b/packages/client/lib/commands/MIGRATE.spec.ts index 880d59a09c7..dd2fbdc82ff 100644 --- a/packages/client/lib/commands/MIGRATE.spec.ts +++ b/packages/client/lib/commands/MIGRATE.spec.ts @@ -1,25 +1,26 @@ import { strict as assert } from 'node:assert'; import MIGRATE from './MIGRATE'; +import { parseArgs } from './generic-transformers'; describe('MIGRATE', () => { describe('transformArguments', () => { it('single key', () => { assert.deepEqual( - MIGRATE.transformArguments('127.0.0.1', 6379, 'key', 0, 10), + parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10), ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10'] ); }); it('multiple keys', () => { assert.deepEqual( - MIGRATE.transformArguments('127.0.0.1', 6379, ['1', '2'], 0, 10), + parseArgs(MIGRATE, '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( - MIGRATE.transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, { COPY: true }), ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'COPY'] @@ -28,7 +29,7 @@ describe('MIGRATE', () => { it('with REPLACE', () => { assert.deepEqual( - MIGRATE.transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, { REPLACE: true }), ['MIGRATE', '127.0.0.1', '6379', 'key', '0', '10', 'REPLACE'] @@ -38,7 +39,7 @@ describe('MIGRATE', () => { describe('with AUTH', () => { it('password only', () => { assert.deepEqual( - MIGRATE.transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, { AUTH: { password: 'password' } @@ -49,7 +50,7 @@ describe('MIGRATE', () => { it('username & password', () => { assert.deepEqual( - MIGRATE.transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, { AUTH: { username: 'username', password: 'password' @@ -62,7 +63,7 @@ describe('MIGRATE', () => { it('with COPY, REPLACE, AUTH', () => { assert.deepEqual( - MIGRATE.transformArguments('127.0.0.1', 6379, 'key', 0, 10, { + parseArgs(MIGRATE, '127.0.0.1', 6379, 'key', 0, 10, { COPY: true, REPLACE: true, AUTH: { diff --git a/packages/client/lib/commands/MIGRATE.ts b/packages/client/lib/commands/MIGRATE.ts index 4821f93fcfb..15345060aa7 100644 --- a/packages/client/lib/commands/MIGRATE.ts +++ b/packages/client/lib/commands/MIGRATE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; import { AuthOptions } from './AUTH'; @@ -9,7 +10,8 @@ export interface MigrateOptions { export default { IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, host: RedisArgument, port: number, key: RedisArgument | Array, @@ -17,37 +19,37 @@ export default { timeout: number, options?: MigrateOptions ) { - const args = ['MIGRATE', host, port.toString()], - isKeyArray = Array.isArray(key); + parser.push('MIGRATE', host, port.toString()); + const isKeyArray = Array.isArray(key); if (isKeyArray) { - args.push(''); + parser.push(''); } else { - args.push(key); + parser.push(key); } - args.push( + parser.push( destinationDb.toString(), timeout.toString() ); if (options?.COPY) { - args.push('COPY'); + parser.push('COPY'); } if (options?.REPLACE) { - args.push('REPLACE'); + parser.push('REPLACE'); } if (options?.AUTH) { if (options.AUTH.username) { - args.push( + parser.push( 'AUTH2', options.AUTH.username, options.AUTH.password ); } else { - args.push( + parser.push( 'AUTH', options.AUTH.password ); @@ -55,13 +57,9 @@ export default { } if (isKeyArray) { - args.push( - 'KEYS', - ...key - ); + parser.push('KEYS'); + parser.pushVariadic(key); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/MODULE_LIST.spec.ts b/packages/client/lib/commands/MODULE_LIST.spec.ts index 9c1d3874381..0aab973cf21 100644 --- a/packages/client/lib/commands/MODULE_LIST.spec.ts +++ b/packages/client/lib/commands/MODULE_LIST.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import MODULE_LIST from './MODULE_LIST'; +import { parseArgs } from './generic-transformers'; describe('MODULE LIST', () => { it('transformArguments', () => { assert.deepEqual( - MODULE_LIST.transformArguments(), + parseArgs(MODULE_LIST), ['MODULE', 'LIST'] ); }); diff --git a/packages/client/lib/commands/MODULE_LIST.ts b/packages/client/lib/commands/MODULE_LIST.ts index 5ddd4e91ff6..85203138f57 100644 --- a/packages/client/lib/commands/MODULE_LIST.ts +++ b/packages/client/lib/commands/MODULE_LIST.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; export type ModuleListReply = ArrayReply>; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['MODULE', 'LIST']; + parseCommand(parser: CommandParser) { + parser.push('MODULE', 'LIST'); }, transformReply: { 2: (reply: UnwrapReply>) => { diff --git a/packages/client/lib/commands/MODULE_LOAD.spec.ts b/packages/client/lib/commands/MODULE_LOAD.spec.ts index 3b7b9dd00a3..418dd9b5daf 100644 --- a/packages/client/lib/commands/MODULE_LOAD.spec.ts +++ b/packages/client/lib/commands/MODULE_LOAD.spec.ts @@ -1,18 +1,19 @@ import { strict as assert } from 'node:assert'; import MODULE_LOAD from './MODULE_LOAD'; +import { parseArgs } from './generic-transformers'; describe('MODULE LOAD', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - MODULE_LOAD.transformArguments('path'), + parseArgs(MODULE_LOAD, 'path'), ['MODULE', 'LOAD', 'path'] ); }); it('with module args', () => { assert.deepEqual( - MODULE_LOAD.transformArguments('path', ['1', '2']), + parseArgs(MODULE_LOAD, 'path', ['1', '2']), ['MODULE', 'LOAD', 'path', '1', '2'] ); }); diff --git a/packages/client/lib/commands/MODULE_LOAD.ts b/packages/client/lib/commands/MODULE_LOAD.ts index 82c5b81a905..ceb90c1c353 100644 --- a/packages/client/lib/commands/MODULE_LOAD.ts +++ b/packages/client/lib/commands/MODULE_LOAD.ts @@ -1,16 +1,15 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(path: RedisArgument, moduleArguments?: Array) { - const args = ['MODULE', 'LOAD', path]; + parseCommand(parser: CommandParser, path: RedisArgument, moduleArguments?: Array) { + parser.push('MODULE', 'LOAD', path); if (moduleArguments) { - return args.concat(moduleArguments); + parser.push(...moduleArguments); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/MODULE_UNLOAD.spec.ts b/packages/client/lib/commands/MODULE_UNLOAD.spec.ts index e33fb3a5f4f..581f41e03c8 100644 --- a/packages/client/lib/commands/MODULE_UNLOAD.spec.ts +++ b/packages/client/lib/commands/MODULE_UNLOAD.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import MODULE_UNLOAD from './MODULE_UNLOAD'; +import { parseArgs } from './generic-transformers'; describe('MODULE UNLOAD', () => { it('transformArguments', () => { assert.deepEqual( - MODULE_UNLOAD.transformArguments('name'), + parseArgs(MODULE_UNLOAD, 'name'), ['MODULE', 'UNLOAD', 'name'] ); }); diff --git a/packages/client/lib/commands/MODULE_UNLOAD.ts b/packages/client/lib/commands/MODULE_UNLOAD.ts index 3a2bc05c0e7..1acc359d0d4 100644 --- a/packages/client/lib/commands/MODULE_UNLOAD.ts +++ b/packages/client/lib/commands/MODULE_UNLOAD.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(name: RedisArgument) { - return ['MODULE', 'UNLOAD', name]; + parseCommand(parser: CommandParser, name: RedisArgument) { + parser.push('MODULE', 'UNLOAD', name); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/MOVE.spec.ts b/packages/client/lib/commands/MOVE.spec.ts index e767568e72b..91a01378b22 100644 --- a/packages/client/lib/commands/MOVE.spec.ts +++ b/packages/client/lib/commands/MOVE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MOVE from './MOVE'; +import { parseArgs } from './generic-transformers'; describe('MOVE', () => { it('transformArguments', () => { assert.deepEqual( - MOVE.transformArguments('key', 1), + parseArgs(MOVE, 'key', 1), ['MOVE', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/MOVE.ts b/packages/client/lib/commands/MOVE.ts index 60aac4b1457..8a6c5427fbc 100644 --- a/packages/client/lib/commands/MOVE.ts +++ b/packages/client/lib/commands/MOVE.ts @@ -1,9 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, db: number) { - return ['MOVE', key, db.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, db: number) { + parser.push('MOVE'); + parser.pushKey(key); + parser.push(db.toString()); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/MSET.spec.ts b/packages/client/lib/commands/MSET.spec.ts index 5435e283108..cfb14eceb05 100644 --- a/packages/client/lib/commands/MSET.spec.ts +++ b/packages/client/lib/commands/MSET.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MSET from './MSET'; +import { parseArgs } from './generic-transformers'; describe('MSET', () => { describe('transformArguments', () => { it("['key1', 'value1', 'key2', 'value2']", () => { assert.deepEqual( - MSET.transformArguments(['key1', 'value1', 'key2', 'value2']), + parseArgs(MSET, ['key1', 'value1', 'key2', 'value2']), ['MSET', 'key1', 'value1', 'key2', 'value2'] ); }); it("[['key1', 'value1'], ['key2', 'value2']]", () => { assert.deepEqual( - MSET.transformArguments([['key1', 'value1'], ['key2', 'value2']]), + parseArgs(MSET, [['key1', 'value1'], ['key2', 'value2']]), ['MSET', 'key1', 'value1', 'key2', 'value2'] ); }); it("{key1: 'value1'. key2: 'value2'}", () => { assert.deepEqual( - MSET.transformArguments({ key1: 'value1', key2: 'value2' }), + parseArgs(MSET, { key1: 'value1', key2: 'value2' }), ['MSET', 'key1', 'value1', 'key2', 'value2'] ); }); diff --git a/packages/client/lib/commands/MSET.ts b/packages/client/lib/commands/MSET.ts index 136fde3e7f7..f761854f09c 100644 --- a/packages/client/lib/commands/MSET.ts +++ b/packages/client/lib/commands/MSET.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export type MSetArguments = @@ -5,23 +6,36 @@ export type MSetArguments = Array | Record; -export function mSetArguments(command: string, toSet: MSetArguments) { - const args: Array = [command]; - +export function parseMSetArguments(parser: CommandParser, toSet: MSetArguments) { if (Array.isArray(toSet)) { - args.push(...toSet.flat()); + if (toSet.length == 0) { + throw new Error("empty toSet Argument") + } + if (Array.isArray(toSet[0])) { + for (const tuple of (toSet as Array<[RedisArgument, RedisArgument]>)) { + parser.pushKey(tuple[0]); + parser.push(tuple[1]); + } + } else { + const arr = toSet as Array; + for (let i=0; i < arr.length; i += 2) { + parser.pushKey(arr[i]); + parser.push(arr[i+1]); + } + } } else { for (const tuple of Object.entries(toSet)) { - args.push(...tuple); + parser.pushKey(tuple[0]); + parser.push(tuple[1]); } } - - return args; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments: mSetArguments.bind(undefined, 'MSET'), + parseCommand(parser: CommandParser, toSet: MSetArguments) { + parser.push('MSET'); + return parseMSetArguments(parser, toSet); + }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/MSETNX.spec.ts b/packages/client/lib/commands/MSETNX.spec.ts index 1583d04a94e..0a9f636abc7 100644 --- a/packages/client/lib/commands/MSETNX.spec.ts +++ b/packages/client/lib/commands/MSETNX.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MSETNX from './MSETNX'; +import { parseArgs } from './generic-transformers'; describe('MSETNX', () => { describe('transformArguments', () => { it("['key1', 'value1', 'key2', 'value2']", () => { assert.deepEqual( - MSETNX.transformArguments(['key1', 'value1', 'key2', 'value2']), + parseArgs(MSETNX, ['key1', 'value1', 'key2', 'value2']), ['MSETNX', 'key1', 'value1', 'key2', 'value2'] ); }); it("[['key1', 'value1'], ['key2', 'value2']]", () => { assert.deepEqual( - MSETNX.transformArguments([['key1', 'value1'], ['key2', 'value2']]), + parseArgs(MSETNX, [['key1', 'value1'], ['key2', 'value2']]), ['MSETNX', 'key1', 'value1', 'key2', 'value2'] ); }); it("{key1: 'value1'. key2: 'value2'}", () => { assert.deepEqual( - MSETNX.transformArguments({ key1: 'value1', key2: 'value2' }), + parseArgs(MSETNX, { key1: 'value1', key2: 'value2' }), ['MSETNX', 'key1', 'value1', 'key2', 'value2'] ); }); diff --git a/packages/client/lib/commands/MSETNX.ts b/packages/client/lib/commands/MSETNX.ts index 4867b3ea092..3ecce9525de 100644 --- a/packages/client/lib/commands/MSETNX.ts +++ b/packages/client/lib/commands/MSETNX.ts @@ -1,9 +1,12 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; -import { mSetArguments } from './MSET'; +import { MSetArguments, parseMSetArguments } from './MSET'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments: mSetArguments.bind(undefined, 'MSETNX'), + parseCommand(parser: CommandParser, toSet: MSetArguments) { + parser.push('MSETNX'); + return parseMSetArguments(parser, toSet); + }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/OBJECT_ENCODING.spec.ts b/packages/client/lib/commands/OBJECT_ENCODING.spec.ts index 48146f12924..34f82be9b8d 100644 --- a/packages/client/lib/commands/OBJECT_ENCODING.spec.ts +++ b/packages/client/lib/commands/OBJECT_ENCODING.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import OBJECT_ENCODING from './OBJECT_ENCODING'; +import { parseArgs } from './generic-transformers'; describe('OBJECT ENCODING', () => { it('transformArguments', () => { assert.deepEqual( - OBJECT_ENCODING.transformArguments('key'), + parseArgs(OBJECT_ENCODING, 'key'), ['OBJECT', 'ENCODING', 'key'] ); }); diff --git a/packages/client/lib/commands/OBJECT_ENCODING.ts b/packages/client/lib/commands/OBJECT_ENCODING.ts index e74c3f99ebd..3a795f6fb64 100644 --- a/packages/client/lib/commands/OBJECT_ENCODING.ts +++ b/packages/client/lib/commands/OBJECT_ENCODING.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['OBJECT', 'ENCODING', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('OBJECT', 'ENCODING'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/OBJECT_FREQ.spec.ts b/packages/client/lib/commands/OBJECT_FREQ.spec.ts index cbad72c308d..081501b12e6 100644 --- a/packages/client/lib/commands/OBJECT_FREQ.spec.ts +++ b/packages/client/lib/commands/OBJECT_FREQ.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import OBJECT_FREQ from './OBJECT_FREQ'; +import { parseArgs } from './generic-transformers'; describe('OBJECT FREQ', () => { it('transformArguments', () => { assert.deepEqual( - OBJECT_FREQ.transformArguments('key'), + parseArgs(OBJECT_FREQ, 'key'), ['OBJECT', 'FREQ', 'key'] ); }); diff --git a/packages/client/lib/commands/OBJECT_FREQ.ts b/packages/client/lib/commands/OBJECT_FREQ.ts index b6757c6c57b..dad1124b101 100644 --- a/packages/client/lib/commands/OBJECT_FREQ.ts +++ b/packages/client/lib/commands/OBJECT_FREQ.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['OBJECT', 'FREQ', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('OBJECT', 'FREQ'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts b/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts index 51866427c81..30d47b8133f 100644 --- a/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts +++ b/packages/client/lib/commands/OBJECT_IDLETIME.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import OBJECT_IDLETIME from './OBJECT_IDLETIME'; +import { parseArgs } from './generic-transformers'; describe('OBJECT IDLETIME', () => { it('transformArguments', () => { assert.deepEqual( - OBJECT_IDLETIME.transformArguments('key'), + parseArgs(OBJECT_IDLETIME, 'key'), ['OBJECT', 'IDLETIME', 'key'] ); }); diff --git a/packages/client/lib/commands/OBJECT_IDLETIME.ts b/packages/client/lib/commands/OBJECT_IDLETIME.ts index f0fef24e2d8..2bd32f4e65d 100644 --- a/packages/client/lib/commands/OBJECT_IDLETIME.ts +++ b/packages/client/lib/commands/OBJECT_IDLETIME.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['OBJECT', 'IDLETIME', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('OBJECT', 'IDLETIME'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts b/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts index f4309786eb1..8bac08a2e5b 100644 --- a/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts +++ b/packages/client/lib/commands/OBJECT_REFCOUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import OBJECT_REFCOUNT from './OBJECT_REFCOUNT'; +import { parseArgs } from './generic-transformers'; describe('OBJECT REFCOUNT', () => { it('transformArguments', () => { assert.deepEqual( - OBJECT_REFCOUNT.transformArguments('key'), + parseArgs(OBJECT_REFCOUNT, 'key'), ['OBJECT', 'REFCOUNT', 'key'] ); }); diff --git a/packages/client/lib/commands/OBJECT_REFCOUNT.ts b/packages/client/lib/commands/OBJECT_REFCOUNT.ts index c8381a76bf7..4bee4dea60c 100644 --- a/packages/client/lib/commands/OBJECT_REFCOUNT.ts +++ b/packages/client/lib/commands/OBJECT_REFCOUNT.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['OBJECT', 'REFCOUNT', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('OBJECT', 'REFCOUNT'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PERSIST.spec.ts b/packages/client/lib/commands/PERSIST.spec.ts index d778a1c0a5f..fff6d7b3a76 100644 --- a/packages/client/lib/commands/PERSIST.spec.ts +++ b/packages/client/lib/commands/PERSIST.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PERSIST from './PERSIST'; +import { parseArgs } from './generic-transformers'; describe('PERSIST', () => { it('transformArguments', () => { assert.deepEqual( - PERSIST.transformArguments('key'), + parseArgs(PERSIST, 'key'), ['PERSIST', 'key'] ); }); diff --git a/packages/client/lib/commands/PERSIST.ts b/packages/client/lib/commands/PERSIST.ts index 64637fabc14..a1d31523664 100644 --- a/packages/client/lib/commands/PERSIST.ts +++ b/packages/client/lib/commands/PERSIST.ts @@ -1,9 +1,10 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument) { - return ['PERSIST', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('PERSIST'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PEXPIRE.spec.ts b/packages/client/lib/commands/PEXPIRE.spec.ts index 61bfe69e9a1..368bc9b4907 100644 --- a/packages/client/lib/commands/PEXPIRE.spec.ts +++ b/packages/client/lib/commands/PEXPIRE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PEXPIRE from './PEXPIRE'; +import { parseArgs } from './generic-transformers'; describe('PEXPIRE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - PEXPIRE.transformArguments('key', 1), + parseArgs(PEXPIRE, 'key', 1), ['PEXPIRE', 'key', '1'] ); }); it('with set option', () => { assert.deepEqual( - PEXPIRE.transformArguments('key', 1, 'GT'), + parseArgs(PEXPIRE, 'key', 1, 'GT'), ['PEXPIRE', 'key', '1', 'GT'] ); }); diff --git a/packages/client/lib/commands/PEXPIRE.ts b/packages/client/lib/commands/PEXPIRE.ts index 4d64281e922..4053f46c8e2 100644 --- a/packages/client/lib/commands/PEXPIRE.ts +++ b/packages/client/lib/commands/PEXPIRE.ts @@ -1,20 +1,21 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, ms: number, mode?: 'NX' | 'XX' | 'GT' | 'LT' ) { - const args = ['PEXPIRE', key, ms.toString()]; + parser.push('PEXPIRE'); + parser.pushKey(key); + parser.push(ms.toString()); if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PEXPIREAT.spec.ts b/packages/client/lib/commands/PEXPIREAT.spec.ts index 117c5b512e7..f1053920403 100644 --- a/packages/client/lib/commands/PEXPIREAT.spec.ts +++ b/packages/client/lib/commands/PEXPIREAT.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PEXPIREAT from './PEXPIREAT'; +import { parseArgs } from './generic-transformers'; describe('PEXPIREAT', () => { describe('transformArguments', () => { it('number', () => { assert.deepEqual( - PEXPIREAT.transformArguments('key', 1), + parseArgs(PEXPIREAT, 'key', 1), ['PEXPIREAT', 'key', '1'] ); }); @@ -14,14 +15,14 @@ describe('PEXPIREAT', () => { it('date', () => { const d = new Date(); assert.deepEqual( - PEXPIREAT.transformArguments('key', d), + parseArgs(PEXPIREAT, 'key', d), ['PEXPIREAT', 'key', d.getTime().toString()] ); }); it('with set option', () => { assert.deepEqual( - PEXPIREAT.transformArguments('key', 1, 'XX'), + parseArgs(PEXPIREAT, 'key', 1, 'XX'), ['PEXPIREAT', 'key', '1', 'XX'] ); }); diff --git a/packages/client/lib/commands/PEXPIREAT.ts b/packages/client/lib/commands/PEXPIREAT.ts index cbae589fba6..e454447c970 100644 --- a/packages/client/lib/commands/PEXPIREAT.ts +++ b/packages/client/lib/commands/PEXPIREAT.ts @@ -1,21 +1,22 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformPXAT } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, msTimestamp: number | Date, mode?: 'NX' | 'XX' | 'GT' | 'LT' ) { - const args = ['PEXPIREAT', key, transformPXAT(msTimestamp)]; + parser.push('PEXPIREAT'); + parser.pushKey(key); + parser.push(transformPXAT(msTimestamp)); if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PEXPIRETIME.spec.ts b/packages/client/lib/commands/PEXPIRETIME.spec.ts index 25a8293ec59..dbfc69e80dc 100644 --- a/packages/client/lib/commands/PEXPIRETIME.spec.ts +++ b/packages/client/lib/commands/PEXPIRETIME.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PEXPIRETIME from './PEXPIRETIME'; +import { parseArgs } from './generic-transformers'; describe('PEXPIRETIME', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - PEXPIRETIME.transformArguments('key'), + parseArgs(PEXPIRETIME, 'key'), ['PEXPIRETIME', 'key'] ); }); diff --git a/packages/client/lib/commands/PEXPIRETIME.ts b/packages/client/lib/commands/PEXPIRETIME.ts index e228351fa0e..b5d04eae230 100644 --- a/packages/client/lib/commands/PEXPIRETIME.ts +++ b/packages/client/lib/commands/PEXPIRETIME.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['PEXPIRETIME', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('PEXPIRETIME'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PFADD.spec.ts b/packages/client/lib/commands/PFADD.spec.ts index 04ba44164df..55c4311e638 100644 --- a/packages/client/lib/commands/PFADD.spec.ts +++ b/packages/client/lib/commands/PFADD.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PFADD from './PFADD'; +import { parseArgs } from './generic-transformers'; describe('PFADD', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - PFADD.transformArguments('key', 'element'), + parseArgs(PFADD, 'key', 'element'), ['PFADD', 'key', 'element'] ); }); it('array', () => { assert.deepEqual( - PFADD.transformArguments('key', ['1', '2']), + parseArgs(PFADD, 'key', ['1', '2']), ['PFADD', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/PFADD.ts b/packages/client/lib/commands/PFADD.ts index d7f198fbe55..94c2d1d5ae6 100644 --- a/packages/client/lib/commands/PFADD.ts +++ b/packages/client/lib/commands/PFADD.ts @@ -1,14 +1,15 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, element?: RedisVariadicArgument) { - const args = ['PFADD', key]; - if (!element) return args; - - return pushVariadicArguments(args, element); + parseCommand(parser: CommandParser, key: RedisArgument, element?: RedisVariadicArgument) { + parser.push('PFADD') + parser.pushKey(key); + if (element) { + parser.pushVariadic(element); + } }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PFCOUNT.spec.ts b/packages/client/lib/commands/PFCOUNT.spec.ts index ebb54ea1d72..aec2ebecf0b 100644 --- a/packages/client/lib/commands/PFCOUNT.spec.ts +++ b/packages/client/lib/commands/PFCOUNT.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PFCOUNT from './PFCOUNT'; +import { parseArgs } from './generic-transformers'; describe('PFCOUNT', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - PFCOUNT.transformArguments('key'), + parseArgs(PFCOUNT, 'key'), ['PFCOUNT', 'key'] ); }); it('array', () => { assert.deepEqual( - PFCOUNT.transformArguments(['1', '2']), + parseArgs(PFCOUNT, ['1', '2']), ['PFCOUNT', '1', '2'] ); }); diff --git a/packages/client/lib/commands/PFCOUNT.ts b/packages/client/lib/commands/PFCOUNT.ts index 5b46eb00d92..46d2e2ed71f 100644 --- a/packages/client/lib/commands/PFCOUNT.ts +++ b/packages/client/lib/commands/PFCOUNT.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisVariadicArgument) { - return pushVariadicArguments(['PFCOUNT'], key); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { + parser.push('PFCOUNT'); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PFMERGE.spec.ts b/packages/client/lib/commands/PFMERGE.spec.ts index bb2444b3ef1..a286e932913 100644 --- a/packages/client/lib/commands/PFMERGE.spec.ts +++ b/packages/client/lib/commands/PFMERGE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PFMERGE from './PFMERGE'; +import { parseArgs } from './generic-transformers'; describe('PFMERGE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - PFMERGE.transformArguments('destination', 'source'), + parseArgs(PFMERGE, 'destination', 'source'), ['PFMERGE', 'destination', 'source'] ); }); it('array', () => { assert.deepEqual( - PFMERGE.transformArguments('destination', ['1', '2']), + parseArgs(PFMERGE, 'destination', ['1', '2']), ['PFMERGE', 'destination', '1', '2'] ); }); diff --git a/packages/client/lib/commands/PFMERGE.ts b/packages/client/lib/commands/PFMERGE.ts index eeeeb5173db..e8eccf1afff 100644 --- a/packages/client/lib/commands/PFMERGE.ts +++ b/packages/client/lib/commands/PFMERGE.ts @@ -1,16 +1,18 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( + parseCommand( + parser: CommandParser, destination: RedisArgument, - source?: RedisVariadicArgument + sources?: RedisVariadicArgument ) { - const args = ['PFMERGE', destination]; - if (!source) return args; - - return pushVariadicArguments(args, source); + parser.push('PFMERGE'); + parser.pushKey(destination); + if (sources) { + parser.pushKeys(sources); + } }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PING.spec.ts b/packages/client/lib/commands/PING.spec.ts index 0cd75a6a8de..56f513685f4 100644 --- a/packages/client/lib/commands/PING.spec.ts +++ b/packages/client/lib/commands/PING.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PING from './PING'; +import { parseArgs } from './generic-transformers'; describe('PING', () => { describe('transformArguments', () => { it('default', () => { assert.deepEqual( - PING.transformArguments(), + parseArgs(PING), ['PING'] ); }); it('with message', () => { assert.deepEqual( - PING.transformArguments('message'), + parseArgs(PING, 'message'), ['PING', 'message'] ); }); diff --git a/packages/client/lib/commands/PING.ts b/packages/client/lib/commands/PING.ts index 7f6fd31047e..26807eeeba4 100644 --- a/packages/client/lib/commands/PING.ts +++ b/packages/client/lib/commands/PING.ts @@ -1,15 +1,14 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(message?: RedisArgument) { - const args: Array = ['PING']; + parseCommand(parser: CommandParser, message?: RedisArgument) { + parser.push('PING'); if (message) { - args.push(message); + parser.push(message); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply | BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PSETEX.spec.ts b/packages/client/lib/commands/PSETEX.spec.ts index fd7bcd2dff2..8580e2f8e9d 100644 --- a/packages/client/lib/commands/PSETEX.spec.ts +++ b/packages/client/lib/commands/PSETEX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PSETEX from './PSETEX'; +import { parseArgs } from './generic-transformers'; describe('PSETEX', () => { it('transformArguments', () => { assert.deepEqual( - PSETEX.transformArguments('key', 1, 'value'), + parseArgs(PSETEX, 'key', 1, 'value'), ['PSETEX', 'key', '1', 'value'] ); }); diff --git a/packages/client/lib/commands/PSETEX.ts b/packages/client/lib/commands/PSETEX.ts index 4e345a1a1c9..03a58546d67 100644 --- a/packages/client/lib/commands/PSETEX.ts +++ b/packages/client/lib/commands/PSETEX.ts @@ -1,18 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( - key: RedisArgument, - ms: number, - value: RedisArgument - ) { - return [ - 'PSETEX', - key, - ms.toString(), - value - ]; + parseCommand(parser: CommandParser, key: RedisArgument, ms: number, value: RedisArgument) { + parser.push('PSETEX'); + parser.pushKey(key); + parser.push(ms.toString(), value); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/PTTL.spec.ts b/packages/client/lib/commands/PTTL.spec.ts index 65a9f5dd0ff..deb04bad97e 100644 --- a/packages/client/lib/commands/PTTL.spec.ts +++ b/packages/client/lib/commands/PTTL.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PTTL from './PTTL'; +import { parseArgs } from './generic-transformers'; describe('PTTL', () => { it('transformArguments', () => { assert.deepEqual( - PTTL.transformArguments('key'), + parseArgs(PTTL, 'key'), ['PTTL', 'key'] ); }); diff --git a/packages/client/lib/commands/PTTL.ts b/packages/client/lib/commands/PTTL.ts index 35854337877..5717c51179f 100644 --- a/packages/client/lib/commands/PTTL.ts +++ b/packages/client/lib/commands/PTTL.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['PTTL', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('PTTL'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PUBLISH.spec.ts b/packages/client/lib/commands/PUBLISH.spec.ts index ec1f108e5ee..930adc8c4d7 100644 --- a/packages/client/lib/commands/PUBLISH.spec.ts +++ b/packages/client/lib/commands/PUBLISH.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PUBLISH from './PUBLISH'; +import { parseArgs } from './generic-transformers'; describe('PUBLISH', () => { it('transformArguments', () => { assert.deepEqual( - PUBLISH.transformArguments('channel', 'message'), + parseArgs(PUBLISH, 'channel', 'message'), ['PUBLISH', 'channel', 'message'] ); }); diff --git a/packages/client/lib/commands/PUBLISH.ts b/packages/client/lib/commands/PUBLISH.ts index e790ff16c4d..557efd18834 100644 --- a/packages/client/lib/commands/PUBLISH.ts +++ b/packages/client/lib/commands/PUBLISH.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, IS_FORWARD_COMMAND: true, - transformArguments(channel: RedisArgument, message: RedisArgument) { - return ['PUBLISH', channel, message]; + parseCommand(parser: CommandParser, channel: RedisArgument, message: RedisArgument) { + parser.push('PUBLISH', channel, message); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts b/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts index 2fe02523ed1..369e339a497 100644 --- a/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts +++ b/packages/client/lib/commands/PUBSUB_CHANNELS.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PUBSUB_CHANNELS from './PUBSUB_CHANNELS'; +import { parseArgs } from './generic-transformers'; describe('PUBSUB CHANNELS', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - PUBSUB_CHANNELS.transformArguments(), + parseArgs(PUBSUB_CHANNELS), ['PUBSUB', 'CHANNELS'] ); }); it('with pattern', () => { assert.deepEqual( - PUBSUB_CHANNELS.transformArguments('patter*'), + parseArgs(PUBSUB_CHANNELS, 'patter*'), ['PUBSUB', 'CHANNELS', 'patter*'] ); }); diff --git a/packages/client/lib/commands/PUBSUB_CHANNELS.ts b/packages/client/lib/commands/PUBSUB_CHANNELS.ts index 4bf7abd75dc..0f53c79a78a 100644 --- a/packages/client/lib/commands/PUBSUB_CHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_CHANNELS.ts @@ -1,16 +1,15 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(pattern?: RedisArgument) { - const args: Array = ['PUBSUB', 'CHANNELS']; + parseCommand(parser: CommandParser, pattern?: RedisArgument) { + parser.push('PUBSUB', 'CHANNELS'); if (pattern) { - args.push(pattern); + parser.push(pattern); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts b/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts index 43a2b4b5c0e..d75256bb43c 100644 --- a/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts +++ b/packages/client/lib/commands/PUBSUB_NUMPAT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PUBSUB_NUMPAT from './PUBSUB_NUMPAT'; +import { parseArgs } from './generic-transformers'; describe('PUBSUB NUMPAT', () => { it('transformArguments', () => { assert.deepEqual( - PUBSUB_NUMPAT.transformArguments(), + parseArgs(PUBSUB_NUMPAT), ['PUBSUB', 'NUMPAT'] ); }); diff --git a/packages/client/lib/commands/PUBSUB_NUMPAT.ts b/packages/client/lib/commands/PUBSUB_NUMPAT.ts index e8a0738dc72..173446e023b 100644 --- a/packages/client/lib/commands/PUBSUB_NUMPAT.ts +++ b/packages/client/lib/commands/PUBSUB_NUMPAT.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['PUBSUB', 'NUMPAT']; + parseCommand(parser: CommandParser) { + parser.push('PUBSUB', 'NUMPAT'); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts b/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts index 151dc219288..11339ae2bb5 100644 --- a/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts +++ b/packages/client/lib/commands/PUBSUB_NUMSUB.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PUBSUB_NUMSUB from './PUBSUB_NUMSUB'; +import { parseArgs } from './generic-transformers'; describe('PUBSUB NUMSUB', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - PUBSUB_NUMSUB.transformArguments(), + parseArgs(PUBSUB_NUMSUB), ['PUBSUB', 'NUMSUB'] ); }); it('string', () => { assert.deepEqual( - PUBSUB_NUMSUB.transformArguments('channel'), + parseArgs(PUBSUB_NUMSUB, 'channel'), ['PUBSUB', 'NUMSUB', 'channel'] ); }); it('array', () => { assert.deepEqual( - PUBSUB_NUMSUB.transformArguments(['1', '2']), + parseArgs(PUBSUB_NUMSUB, ['1', '2']), ['PUBSUB', 'NUMSUB', '1', '2'] ); }); diff --git a/packages/client/lib/commands/PUBSUB_NUMSUB.ts b/packages/client/lib/commands/PUBSUB_NUMSUB.ts index 1f7c41f5bdd..cc74d5d8a73 100644 --- a/packages/client/lib/commands/PUBSUB_NUMSUB.ts +++ b/packages/client/lib/commands/PUBSUB_NUMSUB.ts @@ -1,15 +1,16 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(channels?: RedisVariadicArgument) { - const args = ['PUBSUB', 'NUMSUB']; + parseCommand(parser: CommandParser, channels?: RedisVariadicArgument) { + parser.push('PUBSUB', 'NUMSUB'); - if (channels) return pushVariadicArguments(args, channels); - - return args; + if (channels) { + parser.pushVariadic(channels); + } }, transformReply(rawReply: UnwrapReply>) { const reply = Object.create(null); diff --git a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts index 77982b34670..36597a9cfd8 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PUBSUB_SHARDCHANNELS from './PUBSUB_SHARDCHANNELS'; +import { parseArgs } from './generic-transformers'; describe('PUBSUB SHARDCHANNELS', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,14 +9,14 @@ describe('PUBSUB SHARDCHANNELS', () => { describe('transformArguments', () => { it('without pattern', () => { assert.deepEqual( - PUBSUB_SHARDCHANNELS.transformArguments(), + parseArgs(PUBSUB_SHARDCHANNELS), ['PUBSUB', 'SHARDCHANNELS'] ); }); it('with pattern', () => { assert.deepEqual( - PUBSUB_SHARDCHANNELS.transformArguments('patter*'), + parseArgs(PUBSUB_SHARDCHANNELS, 'patter*'), ['PUBSUB', 'SHARDCHANNELS', 'patter*'] ); }); diff --git a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts index 74d78c02614..46ac2005fc3 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDCHANNELS.ts @@ -1,16 +1,15 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(pattern?: RedisArgument) { - const args: Array = ['PUBSUB', 'SHARDCHANNELS']; + parseCommand(parser: CommandParser, pattern?: RedisArgument) { + parser.push('PUBSUB', 'SHARDCHANNELS'); if (pattern) { - args.push(pattern); + parser.push(pattern); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts index e036a6eae5b..e335941897d 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import PUBSUB_SHARDNUMSUB from './PUBSUB_SHARDNUMSUB'; +import { parseArgs } from './generic-transformers'; describe('PUBSUB SHARDNUMSUB', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,21 +9,21 @@ describe('PUBSUB SHARDNUMSUB', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - PUBSUB_SHARDNUMSUB.transformArguments(), + parseArgs(PUBSUB_SHARDNUMSUB), ['PUBSUB', 'SHARDNUMSUB'] ); }); it('string', () => { assert.deepEqual( - PUBSUB_SHARDNUMSUB.transformArguments('channel'), + parseArgs(PUBSUB_SHARDNUMSUB, 'channel'), ['PUBSUB', 'SHARDNUMSUB', 'channel'] ); }); it('array', () => { assert.deepEqual( - PUBSUB_SHARDNUMSUB.transformArguments(['1', '2']), + parseArgs(PUBSUB_SHARDNUMSUB, ['1', '2']), ['PUBSUB', 'SHARDNUMSUB', '1', '2'] ); }); diff --git a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts index 0ef82477006..220eadeabe3 100644 --- a/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts +++ b/packages/client/lib/commands/PUBSUB_SHARDNUMSUB.ts @@ -1,15 +1,15 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, IS_READ_ONLY: true, - transformArguments(channels?: RedisVariadicArgument) { - const args = ['PUBSUB', 'SHARDNUMSUB']; + parseCommand(parser: CommandParser, channels?: RedisVariadicArgument) { + parser.push('PUBSUB', 'SHARDNUMSUB'); - if (channels) return pushVariadicArguments(args, channels); - - return args; + if (channels) { + parser.pushVariadic(channels); + } }, transformReply(reply: UnwrapReply>) { const transformedReply: Record = Object.create(null); diff --git a/packages/client/lib/commands/RANDOMKEY.spec.ts b/packages/client/lib/commands/RANDOMKEY.spec.ts index 31de60d7a99..f86617a3b75 100644 --- a/packages/client/lib/commands/RANDOMKEY.spec.ts +++ b/packages/client/lib/commands/RANDOMKEY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RANDOMKEY from './RANDOMKEY'; +import { parseArgs } from './generic-transformers'; describe('RANDOMKEY', () => { it('transformArguments', () => { assert.deepEqual( - RANDOMKEY.transformArguments(), + parseArgs(RANDOMKEY), ['RANDOMKEY'] ); }); diff --git a/packages/client/lib/commands/RANDOMKEY.ts b/packages/client/lib/commands/RANDOMKEY.ts index 42028ebbe38..97d040a0d1d 100644 --- a/packages/client/lib/commands/RANDOMKEY.ts +++ b/packages/client/lib/commands/RANDOMKEY.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['RANDOMKEY']; + parseCommand(parser: CommandParser) { + parser.push('RANDOMKEY'); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/READONLY.spec.ts b/packages/client/lib/commands/READONLY.spec.ts index 14bb047349a..ac303322330 100644 --- a/packages/client/lib/commands/READONLY.spec.ts +++ b/packages/client/lib/commands/READONLY.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import READONLY from './READONLY'; +import { parseArgs } from './generic-transformers'; describe('READONLY', () => { it('transformArguments', () => { assert.deepEqual( - READONLY.transformArguments(), + parseArgs(READONLY), ['READONLY'] ); }); diff --git a/packages/client/lib/commands/READONLY.ts b/packages/client/lib/commands/READONLY.ts index bb15834550e..ce3300c5321 100644 --- a/packages/client/lib/commands/READONLY.ts +++ b/packages/client/lib/commands/READONLY.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['READONLY']; + parseCommand(parser: CommandParser) { + parser.push('READONLY'); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/READWRITE.spec.ts b/packages/client/lib/commands/READWRITE.spec.ts index 94a88a0dcc7..cc3f99a5d16 100644 --- a/packages/client/lib/commands/READWRITE.spec.ts +++ b/packages/client/lib/commands/READWRITE.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import READWRITE from './READWRITE'; +import { parseArgs } from './generic-transformers'; describe('READWRITE', () => { it('transformArguments', () => { assert.deepEqual( - READWRITE.transformArguments(), + parseArgs(READWRITE), ['READWRITE'] ); }); diff --git a/packages/client/lib/commands/READWRITE.ts b/packages/client/lib/commands/READWRITE.ts index fe70e15d4c8..7d9d8c7e00a 100644 --- a/packages/client/lib/commands/READWRITE.ts +++ b/packages/client/lib/commands/READWRITE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['READWRITE']; + parseCommand(parser: CommandParser) { + parser.push('READWRITE'); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/RENAME.spec.ts b/packages/client/lib/commands/RENAME.spec.ts index cf3dccbf3e7..05dd9417b96 100644 --- a/packages/client/lib/commands/RENAME.spec.ts +++ b/packages/client/lib/commands/RENAME.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RENAME from './RENAME'; +import { parseArgs } from './generic-transformers'; describe('RENAME', () => { it('transformArguments', () => { assert.deepEqual( - RENAME.transformArguments('source', 'destination'), + parseArgs(RENAME, 'source', 'destination'), ['RENAME', 'source', 'destination'] ); }); diff --git a/packages/client/lib/commands/RENAME.ts b/packages/client/lib/commands/RENAME.ts index 16e883d0532..245851ca31a 100644 --- a/packages/client/lib/commands/RENAME.ts +++ b/packages/client/lib/commands/RENAME.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, newKey: RedisArgument) { - return ['RENAME', key, newKey]; + parseCommand(parser: CommandParser, key: RedisArgument, newKey: RedisArgument) { + parser.push('RENAME'); + parser.pushKeys([key, newKey]); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/RENAMENX.spec.ts b/packages/client/lib/commands/RENAMENX.spec.ts index 5f83933e957..2367b453322 100644 --- a/packages/client/lib/commands/RENAMENX.spec.ts +++ b/packages/client/lib/commands/RENAMENX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RENAMENX from './RENAMENX'; +import { parseArgs } from './generic-transformers'; describe('RENAMENX', () => { it('transformArguments', () => { assert.deepEqual( - RENAMENX.transformArguments('source', 'destination'), + parseArgs(RENAMENX, 'source', 'destination'), ['RENAMENX', 'source', 'destination'] ); }); diff --git a/packages/client/lib/commands/RENAMENX.ts b/packages/client/lib/commands/RENAMENX.ts index 3a4f155d5a7..0e8d4f73cf3 100644 --- a/packages/client/lib/commands/RENAMENX.ts +++ b/packages/client/lib/commands/RENAMENX.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, newKey: RedisArgument) { - return ['RENAMENX', key, newKey]; + parseCommand(parser: CommandParser, key: RedisArgument, newKey: RedisArgument) { + parser.push('RENAMENX'); + parser.pushKeys([key, newKey]); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/REPLICAOF.spec.ts b/packages/client/lib/commands/REPLICAOF.spec.ts index 77dbe060450..13668639494 100644 --- a/packages/client/lib/commands/REPLICAOF.spec.ts +++ b/packages/client/lib/commands/REPLICAOF.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import REPLICAOF from './REPLICAOF'; +import { parseArgs } from './generic-transformers'; describe('REPLICAOF', () => { it('transformArguments', () => { assert.deepEqual( - REPLICAOF.transformArguments('host', 1), + parseArgs(REPLICAOF, 'host', 1), ['REPLICAOF', 'host', '1'] ); }); diff --git a/packages/client/lib/commands/REPLICAOF.ts b/packages/client/lib/commands/REPLICAOF.ts index 4e2f69f7265..c4b09bc4fb8 100644 --- a/packages/client/lib/commands/REPLICAOF.ts +++ b/packages/client/lib/commands/REPLICAOF.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(host: string, port: number) { - return ['REPLICAOF', host, port.toString()]; + parseCommand(parser: CommandParser, host: string, port: number) { + parser.push('REPLICAOF', host, port.toString()); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/RESTORE-ASKING.spec.ts b/packages/client/lib/commands/RESTORE-ASKING.spec.ts index 855196b9776..1258cf68e2d 100644 --- a/packages/client/lib/commands/RESTORE-ASKING.spec.ts +++ b/packages/client/lib/commands/RESTORE-ASKING.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import RESTORE_ASKING from './RESTORE-ASKING'; +import { parseArgs } from './generic-transformers'; describe('RESTORE-ASKING', () => { it('transformArguments', () => { assert.deepEqual( - RESTORE_ASKING.transformArguments(), + parseArgs(RESTORE_ASKING), ['RESTORE-ASKING'] ); }); diff --git a/packages/client/lib/commands/RESTORE-ASKING.ts b/packages/client/lib/commands/RESTORE-ASKING.ts index 14f6dcbeab3..e8de532b6a4 100644 --- a/packages/client/lib/commands/RESTORE-ASKING.ts +++ b/packages/client/lib/commands/RESTORE-ASKING.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['RESTORE-ASKING']; + parseCommand(parser: CommandParser) { + parser.push('RESTORE-ASKING'); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/RESTORE.spec.ts b/packages/client/lib/commands/RESTORE.spec.ts index 6b814e7325a..6083b2eb1a5 100644 --- a/packages/client/lib/commands/RESTORE.spec.ts +++ b/packages/client/lib/commands/RESTORE.spec.ts @@ -2,19 +2,20 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import RESTORE from './RESTORE'; import { RESP_TYPES } from '../RESP/decoder'; +import { parseArgs } from './generic-transformers'; describe('RESTORE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - RESTORE.transformArguments('key', 0, 'value'), + parseArgs(RESTORE, 'key', 0, 'value'), ['RESTORE', 'key', '0', 'value'] ); }); it('with REPLACE', () => { assert.deepEqual( - RESTORE.transformArguments('key', 0, 'value', { + parseArgs(RESTORE, 'key', 0, 'value', { REPLACE: true }), ['RESTORE', 'key', '0', 'value', 'REPLACE'] @@ -23,7 +24,7 @@ describe('RESTORE', () => { it('with ABSTTL', () => { assert.deepEqual( - RESTORE.transformArguments('key', 0, 'value', { + parseArgs(RESTORE, 'key', 0, 'value', { ABSTTL: true }), ['RESTORE', 'key', '0', 'value', 'ABSTTL'] @@ -32,7 +33,7 @@ describe('RESTORE', () => { it('with IDLETIME', () => { assert.deepEqual( - RESTORE.transformArguments('key', 0, 'value', { + parseArgs(RESTORE, 'key', 0, 'value', { IDLETIME: 1 }), ['RESTORE', 'key', '0', 'value', 'IDLETIME', '1'] @@ -41,7 +42,7 @@ describe('RESTORE', () => { it('with FREQ', () => { assert.deepEqual( - RESTORE.transformArguments('key', 0, 'value', { + parseArgs(RESTORE, 'key', 0, 'value', { FREQ: 1 }), ['RESTORE', 'key', '0', 'value', 'FREQ', '1'] @@ -50,7 +51,7 @@ describe('RESTORE', () => { it('with REPLACE, ABSTTL, IDLETIME and FREQ', () => { assert.deepEqual( - RESTORE.transformArguments('key', 0, 'value', { + parseArgs(RESTORE, 'key', 0, 'value', { REPLACE: true, ABSTTL: true, IDLETIME: 1, diff --git a/packages/client/lib/commands/RESTORE.ts b/packages/client/lib/commands/RESTORE.ts index b24c5b569f9..49016c525bd 100644 --- a/packages/client/lib/commands/RESTORE.ts +++ b/packages/client/lib/commands/RESTORE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export interface RestoreOptions { @@ -8,33 +9,33 @@ export interface RestoreOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, ttl: number, serializedValue: RedisArgument, options?: RestoreOptions ) { - const args = ['RESTORE', key, ttl.toString(), serializedValue]; + parser.push('RESTORE'); + parser.pushKey(key); + parser.push(ttl.toString(), serializedValue); if (options?.REPLACE) { - args.push('REPLACE'); + parser.push('REPLACE'); } if (options?.ABSTTL) { - args.push('ABSTTL'); + parser.push('ABSTTL'); } if (options?.IDLETIME) { - args.push('IDLETIME', options.IDLETIME.toString()); + parser.push('IDLETIME', options.IDLETIME.toString()); } if (options?.FREQ) { - args.push('FREQ', options.FREQ.toString()); + parser.push('FREQ', options.FREQ.toString()); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/ROLE.spec.ts b/packages/client/lib/commands/ROLE.spec.ts index c57ba5ba1f0..09ce6ed3427 100644 --- a/packages/client/lib/commands/ROLE.spec.ts +++ b/packages/client/lib/commands/ROLE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ROLE from './ROLE'; +import { parseArgs } from './generic-transformers'; describe('ROLE', () => { it('transformArguments', () => { assert.deepEqual( - ROLE.transformArguments(), + parseArgs(ROLE), ['ROLE'] ); }); diff --git a/packages/client/lib/commands/ROLE.ts b/packages/client/lib/commands/ROLE.ts index 7828e53fb61..f45bbad5c01 100644 --- a/packages/client/lib/commands/ROLE.ts +++ b/packages/client/lib/commands/ROLE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, NumberReply, ArrayReply, TuplesReply, UnwrapReply, Command } from '../RESP/types'; type MasterRole = [ @@ -22,10 +23,10 @@ type SentinelRole = [ type Role = TuplesReply; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['ROLE']; + parseCommand(parser: CommandParser) { + parser.push('ROLE'); }, transformReply(reply: UnwrapReply) { switch (reply[0] as unknown as UnwrapReply) { diff --git a/packages/client/lib/commands/RPOP.spec.ts b/packages/client/lib/commands/RPOP.spec.ts index 8ac5cb290f4..844965eae1a 100644 --- a/packages/client/lib/commands/RPOP.spec.ts +++ b/packages/client/lib/commands/RPOP.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RPOP from './RPOP'; +import { parseArgs } from './generic-transformers'; describe('RPOP', () => { it('transformArguments', () => { assert.deepEqual( - RPOP.transformArguments('key'), + parseArgs(RPOP, 'key'), ['RPOP', 'key'] ); }); diff --git a/packages/client/lib/commands/RPOP.ts b/packages/client/lib/commands/RPOP.ts index f7d0b33d3af..4cc105c3704 100644 --- a/packages/client/lib/commands/RPOP.ts +++ b/packages/client/lib/commands/RPOP.ts @@ -1,9 +1,10 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument) { - return ['RPOP', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('RPOP'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/RPOPLPUSH.spec.ts b/packages/client/lib/commands/RPOPLPUSH.spec.ts index 59458fc0aa8..728d600bc9d 100644 --- a/packages/client/lib/commands/RPOPLPUSH.spec.ts +++ b/packages/client/lib/commands/RPOPLPUSH.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RPOPLPUSH from './RPOPLPUSH'; +import { parseArgs } from './generic-transformers'; describe('RPOPLPUSH', () => { it('transformArguments', () => { assert.deepEqual( - RPOPLPUSH.transformArguments('source', 'destination'), + parseArgs(RPOPLPUSH, 'source', 'destination'), ['RPOPLPUSH', 'source', 'destination'] ); }); diff --git a/packages/client/lib/commands/RPOPLPUSH.ts b/packages/client/lib/commands/RPOPLPUSH.ts index 1a5e1cc59bc..dcac0472235 100644 --- a/packages/client/lib/commands/RPOPLPUSH.ts +++ b/packages/client/lib/commands/RPOPLPUSH.ts @@ -1,12 +1,10 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( - source: RedisArgument, - destination: RedisArgument - ) { - return ['RPOPLPUSH', source, destination]; + parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument) { + parser.push('RPOPLPUSH'); + parser.pushKeys([source, destination]); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/RPOP_COUNT.spec.ts b/packages/client/lib/commands/RPOP_COUNT.spec.ts index 14f1854b8bc..e055d8655b5 100644 --- a/packages/client/lib/commands/RPOP_COUNT.spec.ts +++ b/packages/client/lib/commands/RPOP_COUNT.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RPOP_COUNT from './RPOP_COUNT'; +import { parseArgs } from './generic-transformers'; describe('RPOP COUNT', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - RPOP_COUNT.transformArguments('key', 1), + parseArgs(RPOP_COUNT, 'key', 1), ['RPOP', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/RPOP_COUNT.ts b/packages/client/lib/commands/RPOP_COUNT.ts index b60dec6ab9d..aff91c6a6f7 100644 --- a/packages/client/lib/commands/RPOP_COUNT.ts +++ b/packages/client/lib/commands/RPOP_COUNT.ts @@ -1,9 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, count: number) { - return ['RPOP', key, count.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + parser.push('RPOP'); + parser.pushKey(key); + parser.push(count.toString()); }, transformReply: undefined as unknown as () => ArrayReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/RPUSH.spec.ts b/packages/client/lib/commands/RPUSH.spec.ts index 06078d75951..559fb7a2746 100644 --- a/packages/client/lib/commands/RPUSH.spec.ts +++ b/packages/client/lib/commands/RPUSH.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RPUSH from './RPUSH'; +import { parseArgs } from './generic-transformers'; describe('RPUSH', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - RPUSH.transformArguments('key', 'element'), + parseArgs(RPUSH, 'key', 'element'), ['RPUSH', 'key', 'element'] ); }); it('array', () => { assert.deepEqual( - RPUSH.transformArguments('key', ['1', '2']), + parseArgs(RPUSH, 'key', ['1', '2']), ['RPUSH', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/RPUSH.ts b/packages/client/lib/commands/RPUSH.ts index 4b048777389..b820aae6906 100644 --- a/packages/client/lib/commands/RPUSH.ts +++ b/packages/client/lib/commands/RPUSH.ts @@ -1,13 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( - key: RedisArgument, - element: RedisVariadicArgument - ) { - return pushVariadicArguments(['RPUSH', key], element); + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisVariadicArgument) { + parser.push('RPUSH'); + parser.pushKey(key); + parser.pushVariadic(element); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/RPUSHX.spec.ts b/packages/client/lib/commands/RPUSHX.spec.ts index 5adaa8b263a..b9fb660c5bc 100644 --- a/packages/client/lib/commands/RPUSHX.spec.ts +++ b/packages/client/lib/commands/RPUSHX.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RPUSHX from './RPUSHX'; +import { parseArgs } from './generic-transformers'; describe('RPUSHX', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - RPUSHX.transformArguments('key', 'element'), + parseArgs(RPUSHX, 'key', 'element'), ['RPUSHX', 'key', 'element'] ); }); it('array', () => { assert.deepEqual( - RPUSHX.transformArguments('key', ['1', '2']), + parseArgs(RPUSHX, 'key', ['1', '2']), ['RPUSHX', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/RPUSHX.ts b/packages/client/lib/commands/RPUSHX.ts index 00b29624b0d..243f717bb78 100644 --- a/packages/client/lib/commands/RPUSHX.ts +++ b/packages/client/lib/commands/RPUSHX.ts @@ -1,13 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( - key: RedisArgument, - element: RedisVariadicArgument - ) { - return pushVariadicArguments(['RPUSHX', key], element); + parseCommand(parser: CommandParser, key: RedisArgument, element: RedisVariadicArgument) { + parser.push('RPUSHX'); + parser.pushKey(key); + parser.pushVariadic(element); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SADD.spec.ts b/packages/client/lib/commands/SADD.spec.ts index 77adc1c18ce..179e8602efc 100644 --- a/packages/client/lib/commands/SADD.spec.ts +++ b/packages/client/lib/commands/SADD.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SADD from './SADD'; +import { parseArgs } from './generic-transformers'; describe('SADD', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - SADD.transformArguments('key', 'member'), + parseArgs(SADD, 'key', 'member'), ['SADD', 'key', 'member'] ); }); it('array', () => { assert.deepEqual( - SADD.transformArguments('key', ['1', '2']), + parseArgs(SADD, 'key', ['1', '2']), ['SADD', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SADD.ts b/packages/client/lib/commands/SADD.ts index 2ff5e9263c3..1fb0171d8d4 100644 --- a/packages/client/lib/commands/SADD.ts +++ b/packages/client/lib/commands/SADD.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, members: RedisVariadicArgument) { - return pushVariadicArguments(['SADD', key], members); + parseCommand(parser: CommandParser, key: RedisArgument, members: RedisVariadicArgument) { + parser.push('SADD'); + parser.pushKey(key); + parser.pushVariadic(members); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SAVE.spec.ts b/packages/client/lib/commands/SAVE.spec.ts index 5c014da7edb..5f0074f7492 100644 --- a/packages/client/lib/commands/SAVE.spec.ts +++ b/packages/client/lib/commands/SAVE.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import SAVE from './SAVE'; +import { parseArgs } from './generic-transformers'; describe('SAVE', () => { it('transformArguments', () => { assert.deepEqual( - SAVE.transformArguments(), + parseArgs(SAVE), ['SAVE'] ); }); diff --git a/packages/client/lib/commands/SAVE.ts b/packages/client/lib/commands/SAVE.ts index ee6cccd35a0..ee78884083c 100644 --- a/packages/client/lib/commands/SAVE.ts +++ b/packages/client/lib/commands/SAVE.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['SAVE']; + parseCommand(parser: CommandParser) { + parser.push('SAVE'); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SCAN.spec.ts b/packages/client/lib/commands/SCAN.spec.ts index f4dd865d113..2a32cbebf4f 100644 --- a/packages/client/lib/commands/SCAN.spec.ts +++ b/packages/client/lib/commands/SCAN.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; import SCAN from './SCAN'; describe('SCAN', () => { describe('transformArguments', () => { it('cusror only', () => { assert.deepEqual( - SCAN.transformArguments('0'), + parseArgs(SCAN, '0'), ['SCAN', '0'] ); }); it('with MATCH', () => { assert.deepEqual( - SCAN.transformArguments('0', { + parseArgs(SCAN, '0', { MATCH: 'pattern' }), ['SCAN', '0', 'MATCH', 'pattern'] @@ -22,7 +23,7 @@ describe('SCAN', () => { it('with COUNT', () => { assert.deepEqual( - SCAN.transformArguments('0', { + parseArgs(SCAN, '0', { COUNT: 1 }), ['SCAN', '0', 'COUNT', '1'] @@ -31,7 +32,7 @@ describe('SCAN', () => { it('with TYPE', () => { assert.deepEqual( - SCAN.transformArguments('0', { + parseArgs(SCAN, '0', { TYPE: 'stream' }), ['SCAN', '0', 'TYPE', 'stream'] @@ -40,7 +41,7 @@ describe('SCAN', () => { it('with MATCH & COUNT & TYPE', () => { assert.deepEqual( - SCAN.transformArguments('0', { + parseArgs(SCAN, '0', { MATCH: 'pattern', COUNT: 1, TYPE: 'stream' diff --git a/packages/client/lib/commands/SCAN.ts b/packages/client/lib/commands/SCAN.ts index 13f54440443..2d6e4c35258 100644 --- a/packages/client/lib/commands/SCAN.ts +++ b/packages/client/lib/commands/SCAN.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, CommandArguments, BlobStringReply, ArrayReply, Command } from '../RESP/types'; export interface ScanCommonOptions { @@ -5,6 +6,21 @@ export interface ScanCommonOptions { COUNT?: number; } +export function parseScanArguments( + parser: CommandParser, + cursor: RedisArgument, + options?: ScanOptions +) { + parser.push(cursor); + if (options?.MATCH) { + parser.push('MATCH', options.MATCH); + } + + if (options?.COUNT) { + parser.push('COUNT', options.COUNT.toString()); + } +} + export function pushScanArguments( args: CommandArguments, cursor: RedisArgument, @@ -28,16 +44,15 @@ export interface ScanOptions extends ScanCommonOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(cursor: RedisArgument, options?: ScanOptions) { - const args = pushScanArguments(['SCAN'], cursor, options); + parseCommand(parser: CommandParser, cursor: RedisArgument, options?: ScanOptions) { + parser.push('SCAN'); + parseScanArguments(parser, cursor, options); if (options?.TYPE) { - args.push('TYPE', options.TYPE); + parser.push('TYPE', options.TYPE); } - - return args; }, transformReply([cursor, keys]: [BlobStringReply, ArrayReply]) { return { diff --git a/packages/client/lib/commands/SCARD.spec.ts b/packages/client/lib/commands/SCARD.spec.ts index 5029f340d96..53434583832 100644 --- a/packages/client/lib/commands/SCARD.spec.ts +++ b/packages/client/lib/commands/SCARD.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; import SCARD from './SCARD'; describe('SCARD', () => { it('transformArguments', () => { assert.deepEqual( - SCARD.transformArguments('key'), + parseArgs(SCARD, 'key'), ['SCARD', 'key'] ); }); diff --git a/packages/client/lib/commands/SCARD.ts b/packages/client/lib/commands/SCARD.ts index c13d042ba60..61d4792d996 100644 --- a/packages/client/lib/commands/SCARD.ts +++ b/packages/client/lib/commands/SCARD.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['SCARD', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('SCARD'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts b/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts index 4e07f2c250c..c98143a3415 100644 --- a/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts +++ b/packages/client/lib/commands/SCRIPT_DEBUG.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SCRIPT_DEBUG from './SCRIPT_DEBUG'; +import { parseArgs } from './generic-transformers'; describe('SCRIPT DEBUG', () => { it('transformArguments', () => { assert.deepEqual( - SCRIPT_DEBUG.transformArguments('NO'), + parseArgs(SCRIPT_DEBUG, 'NO'), ['SCRIPT', 'DEBUG', 'NO'] ); }); diff --git a/packages/client/lib/commands/SCRIPT_DEBUG.ts b/packages/client/lib/commands/SCRIPT_DEBUG.ts index 3c49ff709d5..b0d3079068f 100644 --- a/packages/client/lib/commands/SCRIPT_DEBUG.ts +++ b/packages/client/lib/commands/SCRIPT_DEBUG.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(mode: 'YES' | 'SYNC' | 'NO') { - return ['SCRIPT', 'DEBUG', mode]; + parseCommand(parser: CommandParser, mode: 'YES' | 'SYNC' | 'NO') { + parser.push('SCRIPT', 'DEBUG', mode); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts b/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts index 8afdbb5f581..cf65156c72d 100644 --- a/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts +++ b/packages/client/lib/commands/SCRIPT_EXISTS.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SCRIPT_EXISTS from './SCRIPT_EXISTS'; +import { parseArgs } from './generic-transformers'; describe('SCRIPT EXISTS', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - SCRIPT_EXISTS.transformArguments('sha1'), + parseArgs(SCRIPT_EXISTS, 'sha1'), ['SCRIPT', 'EXISTS', 'sha1'] ); }); it('array', () => { assert.deepEqual( - SCRIPT_EXISTS.transformArguments(['1', '2']), + parseArgs(SCRIPT_EXISTS, ['1', '2']), ['SCRIPT', 'EXISTS', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SCRIPT_EXISTS.ts b/packages/client/lib/commands/SCRIPT_EXISTS.ts index ab0a293d8de..b0f6cbe2275 100644 --- a/packages/client/lib/commands/SCRIPT_EXISTS.ts +++ b/packages/client/lib/commands/SCRIPT_EXISTS.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(sha1: RedisVariadicArgument) { - return pushVariadicArguments(['SCRIPT', 'EXISTS'], sha1); + parseCommand(parser: CommandParser, sha1: RedisVariadicArgument) { + parser.push('SCRIPT', 'EXISTS'); + parser.pushVariadic(sha1); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts b/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts index ccc14ecc285..c51efd1a36c 100644 --- a/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts +++ b/packages/client/lib/commands/SCRIPT_FLUSH.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SCRIPT_FLUSH from './SCRIPT_FLUSH'; +import { parseArgs } from './generic-transformers'; describe('SCRIPT FLUSH', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - SCRIPT_FLUSH.transformArguments(), + parseArgs(SCRIPT_FLUSH), ['SCRIPT', 'FLUSH'] ); }); it('with mode', () => { assert.deepEqual( - SCRIPT_FLUSH.transformArguments('SYNC'), + parseArgs(SCRIPT_FLUSH, 'SYNC'), ['SCRIPT', 'FLUSH', 'SYNC'] ); }); diff --git a/packages/client/lib/commands/SCRIPT_FLUSH.ts b/packages/client/lib/commands/SCRIPT_FLUSH.ts index f5426395628..1e05a619bad 100644 --- a/packages/client/lib/commands/SCRIPT_FLUSH.ts +++ b/packages/client/lib/commands/SCRIPT_FLUSH.ts @@ -1,16 +1,15 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(mode?: 'ASYNC' | 'SYNC') { - const args = ['SCRIPT', 'FLUSH']; + parseCommand(parser: CommandParser, mode?: 'ASYNC' | 'SYNC') { + parser.push('SCRIPT', 'FLUSH'); if (mode) { - args.push(mode); + parser.push(mode); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_KILL.spec.ts b/packages/client/lib/commands/SCRIPT_KILL.spec.ts index 1499c97ac07..7186efd54cf 100644 --- a/packages/client/lib/commands/SCRIPT_KILL.spec.ts +++ b/packages/client/lib/commands/SCRIPT_KILL.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import SCRIPT_KILL from './SCRIPT_KILL'; +import { parseArgs } from './generic-transformers'; describe('SCRIPT KILL', () => { it('transformArguments', () => { assert.deepEqual( - SCRIPT_KILL.transformArguments(), + parseArgs(SCRIPT_KILL), ['SCRIPT', 'KILL'] ); }); diff --git a/packages/client/lib/commands/SCRIPT_KILL.ts b/packages/client/lib/commands/SCRIPT_KILL.ts index ac025b788bb..26953506235 100644 --- a/packages/client/lib/commands/SCRIPT_KILL.ts +++ b/packages/client/lib/commands/SCRIPT_KILL.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['SCRIPT', 'KILL']; + parseCommand(parser: CommandParser) { + parser.push('SCRIPT', 'KILL'); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/SCRIPT_LOAD.spec.ts b/packages/client/lib/commands/SCRIPT_LOAD.spec.ts index d964859d2ff..b0df9887e11 100644 --- a/packages/client/lib/commands/SCRIPT_LOAD.spec.ts +++ b/packages/client/lib/commands/SCRIPT_LOAD.spec.ts @@ -2,6 +2,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SCRIPT_LOAD from './SCRIPT_LOAD'; import { scriptSha1 } from '../lua-script'; +import { parseArgs } from './generic-transformers'; describe('SCRIPT LOAD', () => { const SCRIPT = 'return 1;', @@ -9,7 +10,7 @@ describe('SCRIPT LOAD', () => { it('transformArguments', () => { assert.deepEqual( - SCRIPT_LOAD.transformArguments(SCRIPT), + parseArgs(SCRIPT_LOAD, SCRIPT), ['SCRIPT', 'LOAD', SCRIPT] ); }); diff --git a/packages/client/lib/commands/SCRIPT_LOAD.ts b/packages/client/lib/commands/SCRIPT_LOAD.ts index 90028b13a5f..58f7c00dfcd 100644 --- a/packages/client/lib/commands/SCRIPT_LOAD.ts +++ b/packages/client/lib/commands/SCRIPT_LOAD.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command, RedisArgument } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(script: RedisArgument) { - return ['SCRIPT', 'LOAD', script]; + parseCommand(parser: CommandParser, script: RedisArgument) { + parser.push('SCRIPT', 'LOAD', script); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SDIFF.spec.ts b/packages/client/lib/commands/SDIFF.spec.ts index 83ac6dc1da1..a943a80688d 100644 --- a/packages/client/lib/commands/SDIFF.spec.ts +++ b/packages/client/lib/commands/SDIFF.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SDIFF from './SDIFF'; +import { parseArgs } from './generic-transformers'; describe('SDIFF', () => { - describe('transformArguments', () => { + describe('processCommand', () => { it('string', () => { assert.deepEqual( - SDIFF.transformArguments('key'), + parseArgs(SDIFF, 'key'), ['SDIFF', 'key'] ); }); it('array', () => { assert.deepEqual( - SDIFF.transformArguments(['1', '2']), + parseArgs(SDIFF, ['1', '2']), ['SDIFF', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SDIFF.ts b/packages/client/lib/commands/SDIFF.ts index 918cbf7fa15..bd78edc93db 100644 --- a/packages/client/lib/commands/SDIFF.ts +++ b/packages/client/lib/commands/SDIFF.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(keys: RedisVariadicArgument) { - return pushVariadicArguments(['SDIFF'], keys); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { + parser.push('SDIFF'); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SDIFFSTORE.spec.ts b/packages/client/lib/commands/SDIFFSTORE.spec.ts index 613a9eb590b..43213adfbb0 100644 --- a/packages/client/lib/commands/SDIFFSTORE.spec.ts +++ b/packages/client/lib/commands/SDIFFSTORE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SDIFFSTORE from './SDIFFSTORE'; +import { parseArgs } from './generic-transformers'; describe('SDIFFSTORE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - SDIFFSTORE.transformArguments('destination', 'key'), + parseArgs(SDIFFSTORE, 'destination', 'key'), ['SDIFFSTORE', 'destination', 'key'] ); }); it('array', () => { assert.deepEqual( - SDIFFSTORE.transformArguments('destination', ['1', '2']), + parseArgs(SDIFFSTORE, 'destination', ['1', '2']), ['SDIFFSTORE', 'destination', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SDIFFSTORE.ts b/packages/client/lib/commands/SDIFFSTORE.ts index 15f0ccb499a..6da2795d8ff 100644 --- a/packages/client/lib/commands/SDIFFSTORE.ts +++ b/packages/client/lib/commands/SDIFFSTORE.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(destination: RedisArgument, keys: RedisVariadicArgument) { - return pushVariadicArguments(['SDIFFSTORE', destination], keys); + parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) { + parser.push('SDIFFSTORE'); + parser.pushKey(destination); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SET.spec.ts b/packages/client/lib/commands/SET.spec.ts index 4364eb7391a..b8aa57fe77b 100644 --- a/packages/client/lib/commands/SET.spec.ts +++ b/packages/client/lib/commands/SET.spec.ts @@ -1,20 +1,21 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SET from './SET'; +import { parseArgs } from './generic-transformers'; describe('SET', () => { describe('transformArguments', () => { describe('value', () => { it('string', () => { assert.deepEqual( - SET.transformArguments('key', 'value'), + parseArgs(SET, 'key', 'value'), ['SET', 'key', 'value'] ); }); it('number', () => { assert.deepEqual( - SET.transformArguments('key', 0), + parseArgs(SET, 'key', 0), ['SET', 'key', '0'] ); }); @@ -23,7 +24,7 @@ describe('SET', () => { describe('expiration', () => { it('\'KEEPTTL\'', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { expiration: 'KEEPTTL' }), ['SET', 'key', 'value', 'KEEPTTL'] @@ -32,7 +33,7 @@ describe('SET', () => { it('{ type: \'KEEPTTL\' }', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { expiration: { type: 'KEEPTTL' } @@ -43,7 +44,7 @@ describe('SET', () => { it('{ type: \'EX\' }', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { expiration: { type: 'EX', value: 0 @@ -55,7 +56,7 @@ describe('SET', () => { it('with EX (backwards compatibility)', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { EX: 0 }), ['SET', 'key', 'value', 'EX', '0'] @@ -64,7 +65,7 @@ describe('SET', () => { it('with PX (backwards compatibility)', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { PX: 0 }), ['SET', 'key', 'value', 'PX', '0'] @@ -73,7 +74,7 @@ describe('SET', () => { it('with EXAT (backwards compatibility)', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { EXAT: 0 }), ['SET', 'key', 'value', 'EXAT', '0'] @@ -82,7 +83,7 @@ describe('SET', () => { it('with PXAT (backwards compatibility)', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { PXAT: 0 }), ['SET', 'key', 'value', 'PXAT', '0'] @@ -91,7 +92,7 @@ describe('SET', () => { it('with KEEPTTL (backwards compatibility)', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { KEEPTTL: true }), ['SET', 'key', 'value', 'KEEPTTL'] @@ -102,7 +103,7 @@ describe('SET', () => { describe('condition', () => { it('with condition', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { condition: 'NX' }), ['SET', 'key', 'value', 'NX'] @@ -111,7 +112,7 @@ describe('SET', () => { it('with NX (backwards compatibility)', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { NX: true }), ['SET', 'key', 'value', 'NX'] @@ -120,7 +121,7 @@ describe('SET', () => { it('with XX (backwards compatibility)', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { XX: true }), ['SET', 'key', 'value', 'XX'] @@ -130,7 +131,7 @@ describe('SET', () => { it('with GET', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { GET: true }), ['SET', 'key', 'value', 'GET'] @@ -139,7 +140,7 @@ describe('SET', () => { it('with expiration, condition, GET', () => { assert.deepEqual( - SET.transformArguments('key', 'value', { + parseArgs(SET, 'key', 'value', { expiration: { type: 'EX', value: 0 diff --git a/packages/client/lib/commands/SET.ts b/packages/client/lib/commands/SET.ts index cede62e7055..d2d13c874c4 100644 --- a/packages/client/lib/commands/SET.ts +++ b/packages/client/lib/commands/SET.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, BlobStringReply, NullReply, Command } from '../RESP/types'; export interface SetOptions { @@ -42,50 +43,45 @@ export interface SetOptions { } export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, value: RedisArgument | number, options?: SetOptions) { - const args = [ - 'SET', - key, - typeof value === 'number' ? value.toString() : value - ]; + parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument | number, options?: SetOptions) { + parser.push('SET'); + parser.pushKey(key); + parser.push(typeof value === 'number' ? value.toString() : value); if (options?.expiration) { if (typeof options.expiration === 'string') { - args.push(options.expiration); + parser.push(options.expiration); } else if (options.expiration.type === 'KEEPTTL') { - args.push('KEEPTTL'); + parser.push('KEEPTTL'); } else { - args.push( + parser.push( options.expiration.type, options.expiration.value.toString() ); } } else if (options?.EX !== undefined) { - args.push('EX', options.EX.toString()); + parser.push('EX', options.EX.toString()); } else if (options?.PX !== undefined) { - args.push('PX', options.PX.toString()); + parser.push('PX', options.PX.toString()); } else if (options?.EXAT !== undefined) { - args.push('EXAT', options.EXAT.toString()); + parser.push('EXAT', options.EXAT.toString()); } else if (options?.PXAT !== undefined) { - args.push('PXAT', options.PXAT.toString()); + parser.push('PXAT', options.PXAT.toString()); } else if (options?.KEEPTTL) { - args.push('KEEPTTL'); + parser.push('KEEPTTL'); } if (options?.condition) { - args.push(options.condition); + parser.push(options.condition); } else if (options?.NX) { - args.push('NX'); + parser.push('NX'); } else if (options?.XX) { - args.push('XX'); + parser.push('XX'); } if (options?.GET) { - args.push('GET'); + parser.push('GET'); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SETBIT.spec.ts b/packages/client/lib/commands/SETBIT.spec.ts index e4470bb1528..1eedcc69959 100644 --- a/packages/client/lib/commands/SETBIT.spec.ts +++ b/packages/client/lib/commands/SETBIT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SETBIT from './SETBIT'; +import { parseArgs } from './generic-transformers'; describe('SETBIT', () => { it('transformArguments', () => { assert.deepEqual( - SETBIT.transformArguments('key', 0, 1), + parseArgs(SETBIT, 'key', 0, 1), ['SETBIT', 'key', '0', '1'] ); }); diff --git a/packages/client/lib/commands/SETBIT.ts b/packages/client/lib/commands/SETBIT.ts index 5b3ec6173dc..5cd29260071 100644 --- a/packages/client/lib/commands/SETBIT.ts +++ b/packages/client/lib/commands/SETBIT.ts @@ -1,15 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { BitValue } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( - key: RedisArgument, - offset: number, - value: BitValue - ) { - return ['SETBIT', key, offset.toString(), value.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, offset: number, value: BitValue) { + parser.push('SETBIT'); + parser.pushKey(key); + parser.push(offset.toString(), value.toString()); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SETEX.spec.ts b/packages/client/lib/commands/SETEX.spec.ts index 00f204cc713..7bc934ccd68 100644 --- a/packages/client/lib/commands/SETEX.spec.ts +++ b/packages/client/lib/commands/SETEX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SETEX from './SETEX'; +import { parseArgs } from './generic-transformers'; describe('SETEX', () => { it('transformArguments', () => { assert.deepEqual( - SETEX.transformArguments('key', 1, 'value'), + parseArgs(SETEX, 'key', 1, 'value'), ['SETEX', 'key', '1', 'value'] ); }); diff --git a/packages/client/lib/commands/SETEX.ts b/packages/client/lib/commands/SETEX.ts index bbd77e5d990..5e58b589975 100644 --- a/packages/client/lib/commands/SETEX.ts +++ b/packages/client/lib/commands/SETEX.ts @@ -1,18 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( - key: RedisArgument, - seconds: number, - value: RedisArgument - ) { - return [ - 'SETEX', - key, - seconds.toString(), - value - ]; + parseCommand(parser: CommandParser, key: RedisArgument, seconds: number, value: RedisArgument) { + parser.push('SETEX'); + parser.pushKey(key); + parser.push(seconds.toString(), value); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/SETNX .spec.ts b/packages/client/lib/commands/SETNX .spec.ts index 5cfca29ba62..81a5af3d411 100644 --- a/packages/client/lib/commands/SETNX .spec.ts +++ b/packages/client/lib/commands/SETNX .spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SETNX from './SETNX'; +import { parseArgs } from './generic-transformers'; describe('SETNX', () => { it('transformArguments', () => { assert.deepEqual( - SETNX.transformArguments('key', 'value'), + parseArgs(SETNX, 'key', 'value'), ['SETNX', 'key', 'value'] ); }); diff --git a/packages/client/lib/commands/SETNX.ts b/packages/client/lib/commands/SETNX.ts index 0940efad693..ae60067c28f 100644 --- a/packages/client/lib/commands/SETNX.ts +++ b/packages/client/lib/commands/SETNX.ts @@ -1,9 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments(key: RedisArgument, value: RedisArgument) { - return ['SETNX', key, value]; + parseCommand(parser: CommandParser, key: RedisArgument, value: RedisArgument) { + parser.push('SETNX'); + parser.pushKey(key); + parser.push(value); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SETRANGE.spec.ts b/packages/client/lib/commands/SETRANGE.spec.ts index 01d3545a359..acdab5bcd3b 100644 --- a/packages/client/lib/commands/SETRANGE.spec.ts +++ b/packages/client/lib/commands/SETRANGE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SETRANGE from './SETRANGE'; +import { parseArgs } from './generic-transformers'; describe('SETRANGE', () => { it('transformArguments', () => { assert.deepEqual( - SETRANGE.transformArguments('key', 0, 'value'), + parseArgs(SETRANGE, 'key', 0, 'value'), ['SETRANGE', 'key', '0', 'value'] ); }); diff --git a/packages/client/lib/commands/SETRANGE.ts b/packages/client/lib/commands/SETRANGE.ts index 1951a82c07d..42f4ca01117 100644 --- a/packages/client/lib/commands/SETRANGE.ts +++ b/packages/client/lib/commands/SETRANGE.ts @@ -1,18 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( - key: RedisArgument, - offset: number, - value: RedisArgument - ) { - return [ - 'SETRANGE', - key, - offset.toString(), - value - ]; + parseCommand(parser: CommandParser, key: RedisArgument, offset: number, value: RedisArgument) { + parser.push('SETRANGE'); + parser.pushKey(key); + parser.push(offset.toString(), value); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SHUTDOWN.spec.ts b/packages/client/lib/commands/SHUTDOWN.spec.ts index 7dd46a5d534..9c4ca852ad3 100644 --- a/packages/client/lib/commands/SHUTDOWN.spec.ts +++ b/packages/client/lib/commands/SHUTDOWN.spec.ts @@ -1,18 +1,19 @@ import { strict as assert } from 'node:assert'; import SHUTDOWN from './SHUTDOWN'; +import { parseArgs } from './generic-transformers'; describe('SHUTDOWN', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - SHUTDOWN.transformArguments(), + parseArgs(SHUTDOWN), ['SHUTDOWN'] ); }); it('with mode', () => { assert.deepEqual( - SHUTDOWN.transformArguments({ + parseArgs(SHUTDOWN, { mode: 'NOSAVE' }), ['SHUTDOWN', 'NOSAVE'] @@ -21,7 +22,7 @@ describe('SHUTDOWN', () => { it('with NOW', () => { assert.deepEqual( - SHUTDOWN.transformArguments({ + parseArgs(SHUTDOWN, { NOW: true }), ['SHUTDOWN', 'NOW'] @@ -30,7 +31,7 @@ describe('SHUTDOWN', () => { it('with FORCE', () => { assert.deepEqual( - SHUTDOWN.transformArguments({ + parseArgs(SHUTDOWN, { FORCE: true }), ['SHUTDOWN', 'FORCE'] @@ -39,7 +40,7 @@ describe('SHUTDOWN', () => { it('with ABORT', () => { assert.deepEqual( - SHUTDOWN.transformArguments({ + parseArgs(SHUTDOWN, { ABORT: true }), ['SHUTDOWN', 'ABORT'] diff --git a/packages/client/lib/commands/SHUTDOWN.ts b/packages/client/lib/commands/SHUTDOWN.ts index e0f3d08ce81..33fb3e77301 100644 --- a/packages/client/lib/commands/SHUTDOWN.ts +++ b/packages/client/lib/commands/SHUTDOWN.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export interface ShutdownOptions { @@ -8,28 +9,26 @@ export interface ShutdownOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(options?: ShutdownOptions) { - const args = ['SHUTDOWN'] + parseCommand(parser: CommandParser, options?: ShutdownOptions) { + parser.push('SHUTDOWN'); if (options?.mode) { - args.push(options.mode); + parser.push(options.mode); } if (options?.NOW) { - args.push('NOW'); + parser.push('NOW'); } if (options?.FORCE) { - args.push('FORCE'); + parser.push('FORCE'); } if (options?.ABORT) { - args.push('ABORT'); + parser.push('ABORT'); } - - return args; }, transformReply: undefined as unknown as () => void | SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SINTER.spec.ts b/packages/client/lib/commands/SINTER.spec.ts index 5b66fdd3f89..6ca7b959ca7 100644 --- a/packages/client/lib/commands/SINTER.spec.ts +++ b/packages/client/lib/commands/SINTER.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SINTER from './SINTER'; +import { parseArgs } from './generic-transformers'; describe('SINTER', () => { - describe('transformArguments', () => { + describe('processCommand', () => { it('string', () => { assert.deepEqual( - SINTER.transformArguments('key'), + parseArgs(SINTER, 'key'), ['SINTER', 'key'] ); }); it('array', () => { assert.deepEqual( - SINTER.transformArguments(['1', '2']), + parseArgs(SINTER, ['1', '2']), ['SINTER', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SINTER.ts b/packages/client/lib/commands/SINTER.ts index f3f27de2e38..19ecdbb41ca 100644 --- a/packages/client/lib/commands/SINTER.ts +++ b/packages/client/lib/commands/SINTER.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(keys: RedisVariadicArgument) { - return pushVariadicArguments(['SINTER'], keys); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { + parser.push('SINTER'); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SINTERCARD.spec.ts b/packages/client/lib/commands/SINTERCARD.spec.ts index cddb886088a..51aed13415d 100644 --- a/packages/client/lib/commands/SINTERCARD.spec.ts +++ b/packages/client/lib/commands/SINTERCARD.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SINTERCARD from './SINTERCARD'; +import { parseArgs } from './generic-transformers'; describe('SINTERCARD', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,21 +9,21 @@ describe('SINTERCARD', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - SINTERCARD.transformArguments(['1', '2']), + parseArgs(SINTERCARD, ['1', '2']), ['SINTERCARD', '2', '1', '2'] ); }); it('with limit (backwards compatibility)', () => { assert.deepEqual( - SINTERCARD.transformArguments(['1', '2'], 1), + parseArgs(SINTERCARD, ['1', '2'], 1), ['SINTERCARD', '2', '1', '2', 'LIMIT', '1'] ); }); it('with LIMIT', () => { assert.deepEqual( - SINTERCARD.transformArguments(['1', '2'], { + parseArgs(SINTERCARD, ['1', '2'], { LIMIT: 1 }), ['SINTERCARD', '2', '1', '2', 'LIMIT', '1'] diff --git a/packages/client/lib/commands/SINTERCARD.ts b/packages/client/lib/commands/SINTERCARD.ts index 626bc1048c3..cb9e7d3be3d 100644 --- a/packages/client/lib/commands/SINTERCARD.ts +++ b/packages/client/lib/commands/SINTERCARD.ts @@ -1,26 +1,23 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export interface SInterCardOptions { LIMIT?: number; } export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments( - keys: RedisVariadicArgument, - options?: SInterCardOptions | number // `number` for backwards compatibility - ) { - const args = pushVariadicArgument(['SINTERCARD'], keys); + // option `number` for backwards compatibility + parseCommand(parser: CommandParser, keys: RedisVariadicArgument, options?: SInterCardOptions | number) { + parser.push('SINTERCARD'); + parser.pushKeysLength(keys); if (typeof options === 'number') { // backwards compatibility - args.push('LIMIT', options.toString()); + parser.push('LIMIT', options.toString()); } else if (options?.LIMIT !== undefined) { - args.push('LIMIT', options.LIMIT.toString()); + parser.push('LIMIT', options.LIMIT.toString()); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SINTERSTORE.spec.ts b/packages/client/lib/commands/SINTERSTORE.spec.ts index 05416742ee9..83302a5c829 100644 --- a/packages/client/lib/commands/SINTERSTORE.spec.ts +++ b/packages/client/lib/commands/SINTERSTORE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SINTERSTORE from './SINTERSTORE'; +import { parseArgs } from './generic-transformers'; describe('SINTERSTORE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - SINTERSTORE.transformArguments('destination', 'key'), + parseArgs(SINTERSTORE, 'destination', 'key'), ['SINTERSTORE', 'destination', 'key'] ); }); it('array', () => { assert.deepEqual( - SINTERSTORE.transformArguments('destination', ['1', '2']), + parseArgs(SINTERSTORE, 'destination', ['1', '2']), ['SINTERSTORE', 'destination', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SINTERSTORE.ts b/packages/client/lib/commands/SINTERSTORE.ts index 744e0b18456..06db0af9cb0 100644 --- a/packages/client/lib/commands/SINTERSTORE.ts +++ b/packages/client/lib/commands/SINTERSTORE.ts @@ -1,14 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( - destination: RedisArgument, - keys: RedisVariadicArgument - ) { - return pushVariadicArguments(['SINTERSTORE', destination], keys); + parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) { + parser.push('SINTERSTORE'); + parser.pushKey(destination) + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SISMEMBER.spec.ts b/packages/client/lib/commands/SISMEMBER.spec.ts index 0c1b92614cb..4796475c52c 100644 --- a/packages/client/lib/commands/SISMEMBER.spec.ts +++ b/packages/client/lib/commands/SISMEMBER.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SISMEMBER from './SISMEMBER'; +import { parseArgs } from './generic-transformers'; describe('SISMEMBER', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - SISMEMBER.transformArguments('key', 'member'), + parseArgs(SISMEMBER, 'key', 'member'), ['SISMEMBER', 'key', 'member'] ); }); diff --git a/packages/client/lib/commands/SISMEMBER.ts b/packages/client/lib/commands/SISMEMBER.ts index 0687d19de30..6192ca2605f 100644 --- a/packages/client/lib/commands/SISMEMBER.ts +++ b/packages/client/lib/commands/SISMEMBER.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, member: RedisArgument) { - return ['SISMEMBER', key, member]; + parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { + parser.push('SISMEMBER'); + parser.pushKey(key); + parser.push(member); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SMEMBERS.spec.ts b/packages/client/lib/commands/SMEMBERS.spec.ts index 016b01ff747..6e2582e5abc 100644 --- a/packages/client/lib/commands/SMEMBERS.spec.ts +++ b/packages/client/lib/commands/SMEMBERS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SMEMBERS from './SMEMBERS'; +import { parseArgs } from './generic-transformers'; describe('SMEMBERS', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - SMEMBERS.transformArguments('key'), + parseArgs(SMEMBERS, 'key'), ['SMEMBERS', 'key'] ); }); diff --git a/packages/client/lib/commands/SMEMBERS.ts b/packages/client/lib/commands/SMEMBERS.ts index 391c83af6c6..6d018e999f4 100644 --- a/packages/client/lib/commands/SMEMBERS.ts +++ b/packages/client/lib/commands/SMEMBERS.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, SetReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['SMEMBERS', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('SMEMBERS'); + parser.pushKey(key); }, transformReply: { 2: undefined as unknown as () => ArrayReply, diff --git a/packages/client/lib/commands/SMISMEMBER.spec.ts b/packages/client/lib/commands/SMISMEMBER.spec.ts index 273ab05dd75..deff6912360 100644 --- a/packages/client/lib/commands/SMISMEMBER.spec.ts +++ b/packages/client/lib/commands/SMISMEMBER.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SMISMEMBER from './SMISMEMBER'; +import { parseArgs } from './generic-transformers'; describe('SMISMEMBER', () => { testUtils.isVersionGreaterThanHook([6, 2]); - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - SMISMEMBER.transformArguments('key', ['1', '2']), + parseArgs(SMISMEMBER, 'key', ['1', '2']), ['SMISMEMBER', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SMISMEMBER.ts b/packages/client/lib/commands/SMISMEMBER.ts index bdf48d45ab4..f0f3a143c7f 100644 --- a/packages/client/lib/commands/SMISMEMBER.ts +++ b/packages/client/lib/commands/SMISMEMBER.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, members: Array) { - return ['SMISMEMBER', key, ...members]; + parseCommand(parser: CommandParser, key: RedisArgument, members: Array) { + parser.push('SMISMEMBER'); + parser.pushKey(key); + parser.pushVariadic(members); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SMOVE.spec.ts b/packages/client/lib/commands/SMOVE.spec.ts index 7ff2f773a7b..c68a6e41914 100644 --- a/packages/client/lib/commands/SMOVE.spec.ts +++ b/packages/client/lib/commands/SMOVE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SMOVE from './SMOVE'; +import { parseArgs } from './generic-transformers'; describe('SMOVE', () => { it('transformArguments', () => { assert.deepEqual( - SMOVE.transformArguments('source', 'destination', 'member'), + parseArgs(SMOVE, 'source', 'destination', 'member'), ['SMOVE', 'source', 'destination', 'member'] ); }); diff --git a/packages/client/lib/commands/SMOVE.ts b/packages/client/lib/commands/SMOVE.ts index 183b363fb90..d87eeefdfbf 100644 --- a/packages/client/lib/commands/SMOVE.ts +++ b/packages/client/lib/commands/SMOVE.ts @@ -1,14 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( - source: RedisArgument, - destination: RedisArgument, - member: RedisArgument - ) { - return ['SMOVE', source, destination, member]; + parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, member: RedisArgument) { + parser.push('SMOVE'); + parser.pushKeys([source, destination]); + parser.push(member); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SORT.spec.ts b/packages/client/lib/commands/SORT.spec.ts index 4fce8113755..330b321a1b8 100644 --- a/packages/client/lib/commands/SORT.spec.ts +++ b/packages/client/lib/commands/SORT.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SORT from './SORT'; +import { parseArgs } from './generic-transformers'; describe('SORT', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - SORT.transformArguments('key'), + parseArgs(SORT, 'key'), ['SORT', 'key'] ); }); it('with BY', () => { assert.deepEqual( - SORT.transformArguments('key', { + parseArgs(SORT, 'key', { BY: 'pattern' }), ['SORT', 'key', 'BY', 'pattern'] @@ -22,7 +23,7 @@ describe('SORT', () => { it('with LIMIT', () => { assert.deepEqual( - SORT.transformArguments('key', { + parseArgs(SORT, 'key', { LIMIT: { offset: 0, count: 1 @@ -35,7 +36,7 @@ describe('SORT', () => { describe('with GET', () => { it('string', () => { assert.deepEqual( - SORT.transformArguments('key', { + parseArgs(SORT, 'key', { GET: 'pattern' }), ['SORT', 'key', 'GET', 'pattern'] @@ -44,7 +45,7 @@ describe('SORT', () => { it('array', () => { assert.deepEqual( - SORT.transformArguments('key', { + parseArgs(SORT, 'key', { GET: ['1', '2'] }), ['SORT', 'key', 'GET', '1', 'GET', '2'] @@ -54,7 +55,7 @@ describe('SORT', () => { it('with DIRECTION', () => { assert.deepEqual( - SORT.transformArguments('key', { + parseArgs(SORT, 'key', { DIRECTION: 'ASC' }), ['SORT', 'key', 'ASC'] @@ -63,7 +64,7 @@ describe('SORT', () => { it('with ALPHA', () => { assert.deepEqual( - SORT.transformArguments('key', { + parseArgs(SORT, 'key', { ALPHA: true }), ['SORT', 'key', 'ALPHA'] @@ -72,7 +73,7 @@ describe('SORT', () => { it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => { assert.deepEqual( - SORT.transformArguments('key', { + parseArgs(SORT, 'key', { BY: 'pattern', LIMIT: { offset: 0, diff --git a/packages/client/lib/commands/SORT.ts b/packages/client/lib/commands/SORT.ts index b71383943e9..3738d327d91 100644 --- a/packages/client/lib/commands/SORT.ts +++ b/packages/client/lib/commands/SORT.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; export interface SortOptions { @@ -11,19 +12,19 @@ export interface SortOptions { ALPHA?: boolean; } -export function transformSortArguments( - command: RedisArgument, +export function parseSortArguments( + parser: CommandParser, key: RedisArgument, options?: SortOptions ) { - const args: Array = [command, key]; + parser.pushKey(key); if (options?.BY) { - args.push('BY', options.BY); + parser.push('BY', options.BY); } if (options?.LIMIT) { - args.push( + parser.push( 'LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString() @@ -33,27 +34,27 @@ export function transformSortArguments( if (options?.GET) { if (Array.isArray(options.GET)) { for (const pattern of options.GET) { - args.push('GET', pattern); + parser.push('GET', pattern); } } else { - args.push('GET', options.GET); + parser.push('GET', options.GET); } } if (options?.DIRECTION) { - args.push(options.DIRECTION); + parser.push(options.DIRECTION); } if (options?.ALPHA) { - args.push('ALPHA'); + parser.push('ALPHA'); } - - return args; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments: transformSortArguments.bind(undefined, 'SORT'), + parseCommand(parser: CommandParser, key: RedisArgument, options?: SortOptions) { + parser.push('SORT'); + parseSortArguments(parser, key, options); + }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SORT_RO.spec.ts b/packages/client/lib/commands/SORT_RO.spec.ts index 963416ae639..86f8e507033 100644 --- a/packages/client/lib/commands/SORT_RO.spec.ts +++ b/packages/client/lib/commands/SORT_RO.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SORT_RO from './SORT_RO'; +import { parseArgs } from './generic-transformers'; describe('SORT_RO', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,14 +9,14 @@ describe('SORT_RO', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - SORT_RO.transformArguments('key'), + parseArgs(SORT_RO, 'key'), ['SORT_RO', 'key'] ); }); it('with BY', () => { assert.deepEqual( - SORT_RO.transformArguments('key', { + parseArgs(SORT_RO, 'key', { BY: 'pattern' }), ['SORT_RO', 'key', 'BY', 'pattern'] @@ -24,7 +25,7 @@ describe('SORT_RO', () => { it('with LIMIT', () => { assert.deepEqual( - SORT_RO.transformArguments('key', { + parseArgs(SORT_RO, 'key', { LIMIT: { offset: 0, count: 1 @@ -37,7 +38,7 @@ describe('SORT_RO', () => { describe('with GET', () => { it('string', () => { assert.deepEqual( - SORT_RO.transformArguments('key', { + parseArgs(SORT_RO, 'key', { GET: 'pattern' }), ['SORT_RO', 'key', 'GET', 'pattern'] @@ -46,7 +47,7 @@ describe('SORT_RO', () => { it('array', () => { assert.deepEqual( - SORT_RO.transformArguments('key', { + parseArgs(SORT_RO, 'key', { GET: ['1', '2'] }), ['SORT_RO', 'key', 'GET', '1', 'GET', '2'] @@ -56,7 +57,7 @@ describe('SORT_RO', () => { it('with DIRECTION', () => { assert.deepEqual( - SORT_RO.transformArguments('key', { + parseArgs(SORT_RO, 'key', { DIRECTION: 'ASC' }), ['SORT_RO', 'key', 'ASC'] @@ -65,7 +66,7 @@ describe('SORT_RO', () => { it('with ALPHA', () => { assert.deepEqual( - SORT_RO.transformArguments('key', { + parseArgs(SORT_RO, 'key', { ALPHA: true }), ['SORT_RO', 'key', 'ALPHA'] @@ -74,7 +75,7 @@ describe('SORT_RO', () => { it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => { assert.deepEqual( - SORT_RO.transformArguments('key', { + parseArgs(SORT_RO, 'key', { BY: 'pattern', LIMIT: { offset: 0, diff --git a/packages/client/lib/commands/SORT_RO.ts b/packages/client/lib/commands/SORT_RO.ts index 459a0bbc03d..9901907c223 100644 --- a/packages/client/lib/commands/SORT_RO.ts +++ b/packages/client/lib/commands/SORT_RO.ts @@ -1,9 +1,13 @@ import { Command } from '../RESP/types'; -import SORT, { transformSortArguments } from './SORT'; +import SORT, { parseSortArguments } from './SORT'; export default { - FIRST_KEY_INDEX: SORT.FIRST_KEY_INDEX, IS_READ_ONLY: true, - transformArguments: transformSortArguments.bind(undefined, 'SORT_RO'), + parseCommand(...args: Parameters) { + const parser = args[0]; + + parser.push('SORT_RO'); + parseSortArguments(...args); + }, transformReply: SORT.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SORT_STORE.spec.ts b/packages/client/lib/commands/SORT_STORE.spec.ts index 49efd4c6ad8..a812cec52c5 100644 --- a/packages/client/lib/commands/SORT_STORE.spec.ts +++ b/packages/client/lib/commands/SORT_STORE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SORT_STORE from './SORT_STORE'; +import { parseArgs } from './generic-transformers'; describe('SORT STORE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - SORT_STORE.transformArguments('source', 'destination'), + parseArgs(SORT_STORE, 'source', 'destination'), ['SORT', 'source', 'STORE', 'destination'] ); }); it('with BY', () => { assert.deepEqual( - SORT_STORE.transformArguments('source', 'destination', { + parseArgs(SORT_STORE, 'source', 'destination', { BY: 'pattern' }), ['SORT', 'source', 'BY', 'pattern', 'STORE', 'destination'] @@ -22,7 +23,7 @@ describe('SORT STORE', () => { it('with LIMIT', () => { assert.deepEqual( - SORT_STORE.transformArguments('source', 'destination', { + parseArgs(SORT_STORE, 'source', 'destination', { LIMIT: { offset: 0, count: 1 @@ -35,7 +36,7 @@ describe('SORT STORE', () => { describe('with GET', () => { it('string', () => { assert.deepEqual( - SORT_STORE.transformArguments('source', 'destination', { + parseArgs(SORT_STORE, 'source', 'destination', { GET: 'pattern' }), ['SORT', 'source', 'GET', 'pattern', 'STORE', 'destination'] @@ -44,7 +45,7 @@ describe('SORT STORE', () => { it('array', () => { assert.deepEqual( - SORT_STORE.transformArguments('source', 'destination', { + parseArgs(SORT_STORE, 'source', 'destination', { GET: ['1', '2'] }), ['SORT', 'source', 'GET', '1', 'GET', '2', 'STORE', 'destination'] @@ -54,7 +55,7 @@ describe('SORT STORE', () => { it('with DIRECTION', () => { assert.deepEqual( - SORT_STORE.transformArguments('source', 'destination', { + parseArgs(SORT_STORE, 'source', 'destination', { DIRECTION: 'ASC' }), ['SORT', 'source', 'ASC', 'STORE', 'destination'] @@ -63,7 +64,7 @@ describe('SORT STORE', () => { it('with ALPHA', () => { assert.deepEqual( - SORT_STORE.transformArguments('source', 'destination', { + parseArgs(SORT_STORE, 'source', 'destination', { ALPHA: true }), ['SORT', 'source', 'ALPHA', 'STORE', 'destination'] @@ -72,7 +73,7 @@ describe('SORT STORE', () => { it('with BY, LIMIT, GET, DIRECTION, ALPHA', () => { assert.deepEqual( - SORT_STORE.transformArguments('source', 'destination', { + parseArgs(SORT_STORE, 'source', 'destination', { BY: 'pattern', LIMIT: { offset: 0, diff --git a/packages/client/lib/commands/SORT_STORE.ts b/packages/client/lib/commands/SORT_STORE.ts index b6ad709fb60..15c94732e41 100644 --- a/packages/client/lib/commands/SORT_STORE.ts +++ b/packages/client/lib/commands/SORT_STORE.ts @@ -1,17 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; import SORT, { SortOptions } from './SORT'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( - source: RedisArgument, - destination: RedisArgument, - options?: SortOptions - ) { - const args = SORT.transformArguments(source, options); - args.push('STORE', destination); - return args; + parseCommand(parser: CommandParser, source: RedisArgument, destination: RedisArgument, options?: SortOptions) { + SORT.parseCommand(parser, source, options); + parser.push('STORE', destination); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SPOP.spec.ts b/packages/client/lib/commands/SPOP.spec.ts index 896c1c820ac..f435134416b 100644 --- a/packages/client/lib/commands/SPOP.spec.ts +++ b/packages/client/lib/commands/SPOP.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SPOP from './SPOP'; +import { parseArgs } from './generic-transformers'; describe('SPOP', () => { it('transformArguments', () => { assert.deepEqual( - SPOP.transformArguments('key'), + parseArgs(SPOP, 'key'), ['SPOP', 'key'] ); }); diff --git a/packages/client/lib/commands/SPOP.ts b/packages/client/lib/commands/SPOP.ts index 4b061a86306..38f40989e63 100644 --- a/packages/client/lib/commands/SPOP.ts +++ b/packages/client/lib/commands/SPOP.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument) { - return ['SPOP', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('SPOP'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SPOP_COUNT.spec.ts b/packages/client/lib/commands/SPOP_COUNT.spec.ts index ddad816b420..935ff437800 100644 --- a/packages/client/lib/commands/SPOP_COUNT.spec.ts +++ b/packages/client/lib/commands/SPOP_COUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SPOP_COUNT from './SPOP_COUNT'; +import { parseArgs } from './generic-transformers'; describe('SPOP_COUNT', () => { it('transformArguments', () => { assert.deepEqual( - SPOP_COUNT.transformArguments('key', 1), + parseArgs(SPOP_COUNT, 'key', 1), ['SPOP', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/SPOP_COUNT.ts b/packages/client/lib/commands/SPOP_COUNT.ts index 4c68ae8d08e..0536203be97 100644 --- a/packages/client/lib/commands/SPOP_COUNT.ts +++ b/packages/client/lib/commands/SPOP_COUNT.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, count: number) { - return ['SPOP', key, count.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + parser.push('SPOP'); + parser.pushKey(key); + parser.push(count.toString()); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SPUBLISH.spec.ts b/packages/client/lib/commands/SPUBLISH.spec.ts index 741372d0154..5a53bc40b7d 100644 --- a/packages/client/lib/commands/SPUBLISH.spec.ts +++ b/packages/client/lib/commands/SPUBLISH.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SPUBLISH from './SPUBLISH'; +import { parseArgs } from './generic-transformers'; describe('SPUBLISH', () => { testUtils.isVersionGreaterThanHook([7]); it('transformArguments', () => { assert.deepEqual( - SPUBLISH.transformArguments('channel', 'message'), + parseArgs(SPUBLISH, 'channel', 'message'), ['SPUBLISH', 'channel', 'message'] ); }); diff --git a/packages/client/lib/commands/SPUBLISH.ts b/packages/client/lib/commands/SPUBLISH.ts index 19d84b03c6f..77d93e617de 100644 --- a/packages/client/lib/commands/SPUBLISH.ts +++ b/packages/client/lib/commands/SPUBLISH.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(channel: RedisArgument, message: RedisArgument) { - return ['SPUBLISH', channel, message]; + parseCommand(parser: CommandParser, channel: RedisArgument, message: RedisArgument) { + parser.push('SPUBLISH'); + parser.pushKey(channel); + parser.push(message); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SRANDMEMBER.spec.ts b/packages/client/lib/commands/SRANDMEMBER.spec.ts index a7df01f0eb9..637aac27b29 100644 --- a/packages/client/lib/commands/SRANDMEMBER.spec.ts +++ b/packages/client/lib/commands/SRANDMEMBER.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SRANDMEMBER from './SRANDMEMBER'; +import { parseArgs } from './generic-transformers'; describe('SRANDMEMBER', () => { it('transformArguments', () => { assert.deepEqual( - SRANDMEMBER.transformArguments('key'), + parseArgs(SRANDMEMBER, 'key'), ['SRANDMEMBER', 'key'] ); }); diff --git a/packages/client/lib/commands/SRANDMEMBER.ts b/packages/client/lib/commands/SRANDMEMBER.ts index 6a2373ae927..4285f7aa17c 100644 --- a/packages/client/lib/commands/SRANDMEMBER.ts +++ b/packages/client/lib/commands/SRANDMEMBER.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['SRANDMEMBER', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('SRANDMEMBER') + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts b/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts index 364eb640341..13bb0d52d96 100644 --- a/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts +++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SRANDMEMBER_COUNT from './SRANDMEMBER_COUNT'; +import { parseArgs } from './generic-transformers'; describe('SRANDMEMBER COUNT', () => { it('transformArguments', () => { assert.deepEqual( - SRANDMEMBER_COUNT.transformArguments('key', 1), + parseArgs(SRANDMEMBER_COUNT, 'key', 1), ['SRANDMEMBER', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts index 778f3d8f629..dd72245c3b3 100644 --- a/packages/client/lib/commands/SRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/SRANDMEMBER_COUNT.ts @@ -1,13 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; import SRANDMEMBER from './SRANDMEMBER'; export default { - FIRST_KEY_INDEX: SRANDMEMBER.FIRST_KEY_INDEX, IS_READ_ONLY: SRANDMEMBER.IS_READ_ONLY, - transformArguments(key: RedisArgument, count: number) { - const args = SRANDMEMBER.transformArguments(key); - args.push(count.toString()); - return args; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + SRANDMEMBER.parseCommand(parser, key); + parser.push(count.toString()); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SREM.spec.ts b/packages/client/lib/commands/SREM.spec.ts index f17c6fb118e..6def4178fc8 100644 --- a/packages/client/lib/commands/SREM.spec.ts +++ b/packages/client/lib/commands/SREM.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SREM from './SREM'; +import { parseArgs } from './generic-transformers'; describe('SREM', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - SREM.transformArguments('key', 'member'), + parseArgs(SREM, 'key', 'member'), ['SREM', 'key', 'member'] ); }); it('array', () => { assert.deepEqual( - SREM.transformArguments('key', ['1', '2']), + parseArgs(SREM, 'key', ['1', '2']), ['SREM', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SREM.ts b/packages/client/lib/commands/SREM.ts index daa95493d02..75053474cce 100644 --- a/packages/client/lib/commands/SREM.ts +++ b/packages/client/lib/commands/SREM.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, members: RedisVariadicArgument) { - return pushVariadicArguments(['SREM', key], members); + parseCommand(parser: CommandParser, key: RedisArgument, members: RedisVariadicArgument) { + parser.push('SREM'); + parser.pushKey(key); + parser.pushVariadic(members); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SSCAN.spec.ts b/packages/client/lib/commands/SSCAN.spec.ts index 29a13306fde..e5d689c6e98 100644 --- a/packages/client/lib/commands/SSCAN.spec.ts +++ b/packages/client/lib/commands/SSCAN.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SSCAN from './SSCAN'; +import { parseArgs } from './generic-transformers'; describe('SSCAN', () => { describe('transformArguments', () => { it('cusror only', () => { assert.deepEqual( - SSCAN.transformArguments('key', '0'), + parseArgs(SSCAN, 'key', '0'), ['SSCAN', 'key', '0'] ); }); it('with MATCH', () => { assert.deepEqual( - SSCAN.transformArguments('key', '0', { + parseArgs(SSCAN, 'key', '0', { MATCH: 'pattern' }), ['SSCAN', 'key', '0', 'MATCH', 'pattern'] @@ -22,7 +23,7 @@ describe('SSCAN', () => { it('with COUNT', () => { assert.deepEqual( - SSCAN.transformArguments('key', '0', { + parseArgs(SSCAN, 'key', '0', { COUNT: 1 }), ['SSCAN', 'key', '0', 'COUNT', '1'] @@ -31,7 +32,7 @@ describe('SSCAN', () => { it('with MATCH & COUNT', () => { assert.deepEqual( - SSCAN.transformArguments('key', '0', { + parseArgs(SSCAN, 'key', '0', { MATCH: 'pattern', COUNT: 1 }), diff --git a/packages/client/lib/commands/SSCAN.ts b/packages/client/lib/commands/SSCAN.ts index f47144d834c..22634d56242 100644 --- a/packages/client/lib/commands/SSCAN.ts +++ b/packages/client/lib/commands/SSCAN.ts @@ -1,15 +1,18 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; -import { ScanCommonOptions, pushScanArguments } from './SCAN'; +import { ScanCommonOptions, parseScanArguments} from './SCAN'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, cursor: RedisArgument, options?: ScanCommonOptions ) { - return pushScanArguments(['SSCAN', key], cursor, options); + parser.push('SSCAN'); + parser.pushKey(key); + parseScanArguments(parser, cursor, options); }, transformReply([cursor, members]: [BlobStringReply, Array]) { return { diff --git a/packages/client/lib/commands/STRLEN.spec.ts b/packages/client/lib/commands/STRLEN.spec.ts index b07c07b909a..dbb7a08541b 100644 --- a/packages/client/lib/commands/STRLEN.spec.ts +++ b/packages/client/lib/commands/STRLEN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import STRLEN from './STRLEN'; +import { parseArgs } from './generic-transformers'; describe('STRLEN', () => { it('transformArguments', () => { assert.deepEqual( - STRLEN.transformArguments('key'), + parseArgs(STRLEN, 'key'), ['STRLEN', 'key'] ); }); diff --git a/packages/client/lib/commands/STRLEN.ts b/packages/client/lib/commands/STRLEN.ts index 594530ff6bf..34e0430fc9e 100644 --- a/packages/client/lib/commands/STRLEN.ts +++ b/packages/client/lib/commands/STRLEN.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['STRLEN', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('STRLEN'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SUNION.spec.ts b/packages/client/lib/commands/SUNION.spec.ts index ff00c44a1b1..a4389d4236e 100644 --- a/packages/client/lib/commands/SUNION.spec.ts +++ b/packages/client/lib/commands/SUNION.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SUNION from './SUNION'; +import { parseArgs } from './generic-transformers'; describe('SUNION', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - SUNION.transformArguments('key'), + parseArgs(SUNION, 'key'), ['SUNION', 'key'] ); }); it('array', () => { assert.deepEqual( - SUNION.transformArguments(['1', '2']), + parseArgs(SUNION, ['1', '2']), ['SUNION', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SUNION.ts b/packages/client/lib/commands/SUNION.ts index 42042217e2b..3d9a5954a7c 100644 --- a/packages/client/lib/commands/SUNION.ts +++ b/packages/client/lib/commands/SUNION.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(keys: RedisVariadicArgument) { - return pushVariadicArguments(['SUNION'], keys); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { + parser.push('SUNION'); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SUNIONSTORE.spec.ts b/packages/client/lib/commands/SUNIONSTORE.spec.ts index 790fd78060a..8f3db2cacd7 100644 --- a/packages/client/lib/commands/SUNIONSTORE.spec.ts +++ b/packages/client/lib/commands/SUNIONSTORE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SUNIONSTORE from './SUNIONSTORE'; +import { parseArgs } from './generic-transformers'; describe('SUNIONSTORE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - SUNIONSTORE.transformArguments('destination', 'key'), + parseArgs(SUNIONSTORE, 'destination', 'key'), ['SUNIONSTORE', 'destination', 'key'] ); }); it('array', () => { assert.deepEqual( - SUNIONSTORE.transformArguments('destination', ['1', '2']), + parseArgs(SUNIONSTORE, 'destination', ['1', '2']), ['SUNIONSTORE', 'destination', '1', '2'] ); }); diff --git a/packages/client/lib/commands/SUNIONSTORE.ts b/packages/client/lib/commands/SUNIONSTORE.ts index 9adaa9288f3..e2f43ecb1c8 100644 --- a/packages/client/lib/commands/SUNIONSTORE.ts +++ b/packages/client/lib/commands/SUNIONSTORE.ts @@ -1,14 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( - destination: RedisArgument, - keys: RedisVariadicArgument - ) { - return pushVariadicArguments(['SUNIONSTORE', destination], keys); + parseCommand(parser: CommandParser, destination: RedisArgument, keys: RedisVariadicArgument) { + parser.push('SUNIONSTORE'); + parser.pushKey(destination); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/SWAPDB.spec.ts b/packages/client/lib/commands/SWAPDB.spec.ts index 9331854c13b..a3b53b27218 100644 --- a/packages/client/lib/commands/SWAPDB.spec.ts +++ b/packages/client/lib/commands/SWAPDB.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SWAPDB from './SWAPDB'; +import { parseArgs } from './generic-transformers'; describe('SWAPDB', () => { it('transformArguments', () => { assert.deepEqual( - SWAPDB.transformArguments(0, 1), + parseArgs(SWAPDB, 0, 1), ['SWAPDB', '0', '1'] ); }); diff --git a/packages/client/lib/commands/SWAPDB.ts b/packages/client/lib/commands/SWAPDB.ts index f3b768e95f4..e59c75715cd 100644 --- a/packages/client/lib/commands/SWAPDB.ts +++ b/packages/client/lib/commands/SWAPDB.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(index1: number, index2: number) { - return ['SWAPDB', index1.toString(), index2.toString()]; + parseCommand(parser: CommandParser, index1: number, index2: number) { + parser.push('SWAPDB', index1.toString(), index2.toString()); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/TIME.spec.ts b/packages/client/lib/commands/TIME.spec.ts index d9dd9667ea4..4ee704f0dd0 100644 --- a/packages/client/lib/commands/TIME.spec.ts +++ b/packages/client/lib/commands/TIME.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import TIME from './TIME'; +import { parseArgs } from './generic-transformers'; describe('TIME', () => { it('transformArguments', () => { assert.deepEqual( - TIME.transformArguments(), + parseArgs(TIME), ['TIME'] ); }); diff --git a/packages/client/lib/commands/TIME.ts b/packages/client/lib/commands/TIME.ts index d4dc67ae483..b25af710e1c 100644 --- a/packages/client/lib/commands/TIME.ts +++ b/packages/client/lib/commands/TIME.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { BlobStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['TIME']; + parseCommand(parser: CommandParser) { + parser.push('TIME'); }, transformReply: undefined as unknown as () => [ unixTimestamp: BlobStringReply<`${number}`>, diff --git a/packages/client/lib/commands/TOUCH.spec.ts b/packages/client/lib/commands/TOUCH.spec.ts index 48e77900ee3..69a3498346b 100644 --- a/packages/client/lib/commands/TOUCH.spec.ts +++ b/packages/client/lib/commands/TOUCH.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import TOUCH from './TOUCH'; +import { parseArgs } from './generic-transformers'; describe('TOUCH', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - TOUCH.transformArguments('key'), + parseArgs(TOUCH, 'key'), ['TOUCH', 'key'] ); }); it('array', () => { assert.deepEqual( - TOUCH.transformArguments(['1', '2']), + parseArgs(TOUCH, ['1', '2']), ['TOUCH', '1', '2'] ); }); diff --git a/packages/client/lib/commands/TOUCH.ts b/packages/client/lib/commands/TOUCH.ts index c1c19402f8b..c765c9f8347 100644 --- a/packages/client/lib/commands/TOUCH.ts +++ b/packages/client/lib/commands/TOUCH.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisVariadicArgument) { - return pushVariadicArguments(['TOUCH'], key); + parseCommand(parser: CommandParser, key: RedisVariadicArgument) { + parser.push('TOUCH'); + parser.pushKeys(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/TTL.spec.ts b/packages/client/lib/commands/TTL.spec.ts index 6b709226a2b..4d36053c02e 100644 --- a/packages/client/lib/commands/TTL.spec.ts +++ b/packages/client/lib/commands/TTL.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import TTL from './TTL'; +import { parseArgs } from './generic-transformers'; describe('TTL', () => { it('transformArguments', () => { assert.deepEqual( - TTL.transformArguments('key'), + parseArgs(TTL, 'key'), ['TTL', 'key'] ); }); diff --git a/packages/client/lib/commands/TTL.ts b/packages/client/lib/commands/TTL.ts index 65c3b7b026f..8420089fcb9 100644 --- a/packages/client/lib/commands/TTL.ts +++ b/packages/client/lib/commands/TTL.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['TTL', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TTL'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/TYPE.spec.ts b/packages/client/lib/commands/TYPE.spec.ts index 45cf1cfc1c9..ae7392cdce9 100644 --- a/packages/client/lib/commands/TYPE.spec.ts +++ b/packages/client/lib/commands/TYPE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import TYPE from './TYPE'; +import { parseArgs } from './generic-transformers'; describe('TYPE', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - TYPE.transformArguments('key'), + parseArgs(TYPE, 'key'), ['TYPE', 'key'] ); }); diff --git a/packages/client/lib/commands/TYPE.ts b/packages/client/lib/commands/TYPE.ts index 09f6887492c..ffc592994db 100644 --- a/packages/client/lib/commands/TYPE.ts +++ b/packages/client/lib/commands/TYPE.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['TYPE', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('TYPE'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/UNLINK.spec.ts b/packages/client/lib/commands/UNLINK.spec.ts index 1e374783007..2c32bee8e33 100644 --- a/packages/client/lib/commands/UNLINK.spec.ts +++ b/packages/client/lib/commands/UNLINK.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import UNLINK from './UNLINK'; +import { parseArgs } from './generic-transformers'; describe('UNLINK', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - UNLINK.transformArguments('key'), + parseArgs(UNLINK, 'key'), ['UNLINK', 'key'] ); }); it('array', () => { assert.deepEqual( - UNLINK.transformArguments(['1', '2']), + parseArgs(UNLINK, ['1', '2']), ['UNLINK', '1', '2'] ); }); diff --git a/packages/client/lib/commands/UNLINK.ts b/packages/client/lib/commands/UNLINK.ts index 2346573f397..14d1e700277 100644 --- a/packages/client/lib/commands/UNLINK.ts +++ b/packages/client/lib/commands/UNLINK.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisVariadicArgument) { - return pushVariadicArguments(['UNLINK'], key); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { + parser.push('UNLINK'); + parser.pushKeys(keys); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/WAIT.spec.ts b/packages/client/lib/commands/WAIT.spec.ts index 61b197c90ba..d2778e7967b 100644 --- a/packages/client/lib/commands/WAIT.spec.ts +++ b/packages/client/lib/commands/WAIT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import WAIT from './WAIT'; +import { parseArgs } from './generic-transformers'; describe('WAIT', () => { it('transformArguments', () => { assert.deepEqual( - WAIT.transformArguments(0, 1), + parseArgs(WAIT, 0, 1), ['WAIT', '0', '1'] ); }); diff --git a/packages/client/lib/commands/WAIT.ts b/packages/client/lib/commands/WAIT.ts index 21c39a643e5..df45a12373d 100644 --- a/packages/client/lib/commands/WAIT.ts +++ b/packages/client/lib/commands/WAIT.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(numberOfReplicas: number, timeout: number) { - return ['WAIT', numberOfReplicas.toString(), timeout.toString()]; + parseCommand(parser: CommandParser, numberOfReplicas: number, timeout: number) { + parser.push('WAIT', numberOfReplicas.toString(), timeout.toString()); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XACK.spec.ts b/packages/client/lib/commands/XACK.spec.ts index 81a7954ffd6..4ad60b256d0 100644 --- a/packages/client/lib/commands/XACK.spec.ts +++ b/packages/client/lib/commands/XACK.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XACK from './XACK'; +import { parseArgs } from './generic-transformers'; describe('XACK', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - XACK.transformArguments('key', 'group', '0-0'), + parseArgs(XACK, 'key', 'group', '0-0'), ['XACK', 'key', 'group', '0-0'] ); }); it('array', () => { assert.deepEqual( - XACK.transformArguments('key', 'group', ['0-0', '1-0']), + parseArgs(XACK, 'key', 'group', ['0-0', '1-0']), ['XACK', 'key', 'group', '0-0', '1-0'] ); }); diff --git a/packages/client/lib/commands/XACK.ts b/packages/client/lib/commands/XACK.ts index 89b2d581201..2500134f1c8 100644 --- a/packages/client/lib/commands/XACK.ts +++ b/packages/client/lib/commands/XACK.ts @@ -1,15 +1,15 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( - key: RedisArgument, - group: RedisArgument, - id: RedisVariadicArgument - ) { - return pushVariadicArguments(['XACK', key, group], id); + parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument, id: RedisVariadicArgument) { + parser.push('XACK'); + parser.pushKey(key); + parser.push(group) + parser.pushVariadic(id); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; + \ No newline at end of file diff --git a/packages/client/lib/commands/XADD.spec.ts b/packages/client/lib/commands/XADD.spec.ts index 10c6f4daa56..321581d0865 100644 --- a/packages/client/lib/commands/XADD.spec.ts +++ b/packages/client/lib/commands/XADD.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XADD from './XADD'; +import { parseArgs } from './generic-transformers'; describe('XADD', () => { describe('transformArguments', () => { it('single field', () => { assert.deepEqual( - XADD.transformArguments('key', '*', { + parseArgs(XADD, 'key', '*', { field: 'value' }), ['XADD', 'key', '*', 'field', 'value'] @@ -15,7 +16,7 @@ describe('XADD', () => { it('multiple fields', () => { assert.deepEqual( - XADD.transformArguments('key', '*', { + parseArgs(XADD, 'key', '*', { '1': 'I', '2': 'II' }), @@ -25,7 +26,7 @@ describe('XADD', () => { it('with TRIM', () => { assert.deepEqual( - XADD.transformArguments('key', '*', { + parseArgs(XADD, 'key', '*', { field: 'value' }, { TRIM: { @@ -38,7 +39,7 @@ describe('XADD', () => { it('with TRIM.strategy', () => { assert.deepEqual( - XADD.transformArguments('key', '*', { + parseArgs(XADD, 'key', '*', { field: 'value' }, { TRIM: { @@ -52,7 +53,7 @@ describe('XADD', () => { it('with TRIM.strategyModifier', () => { assert.deepEqual( - XADD.transformArguments('key', '*', { + parseArgs(XADD, 'key', '*', { field: 'value' }, { TRIM: { @@ -66,7 +67,7 @@ describe('XADD', () => { it('with TRIM.limit', () => { assert.deepEqual( - XADD.transformArguments('key', '*', { + parseArgs(XADD, 'key', '*', { field: 'value' }, { TRIM: { diff --git a/packages/client/lib/commands/XADD.ts b/packages/client/lib/commands/XADD.ts index b681069c729..cb9d0f5fad8 100644 --- a/packages/client/lib/commands/XADD.ts +++ b/packages/client/lib/commands/XADD.ts @@ -1,4 +1,6 @@ -import { RedisArgument, BlobStringReply, Command, CommandArguments } from '../RESP/types'; +import { CommandParser } from '../client/parser'; +import { RedisArgument, BlobStringReply, Command } from '../RESP/types'; +import { Tail } from './generic-transformers'; export interface XAddOptions { TRIM?: { @@ -9,47 +11,47 @@ export interface XAddOptions { }; } -export function pushXAddArguments( - args: CommandArguments, +export function parseXAddArguments( + optional: RedisArgument | undefined, + parser: CommandParser, + key: RedisArgument, id: RedisArgument, message: Record, options?: XAddOptions ) { + parser.push('XADD'); + parser.pushKey(key); + if (optional) { + parser.push(optional); + } + if (options?.TRIM) { if (options.TRIM.strategy) { - args.push(options.TRIM.strategy); + parser.push(options.TRIM.strategy); } if (options.TRIM.strategyModifier) { - args.push(options.TRIM.strategyModifier); + parser.push(options.TRIM.strategyModifier); } - args.push(options.TRIM.threshold.toString()); + parser.push(options.TRIM.threshold.toString()); if (options.TRIM.limit) { - args.push('LIMIT', options.TRIM.limit.toString()); + parser.push('LIMIT', options.TRIM.limit.toString()); } } - args.push(id); + parser.push(id); for (const [key, value] of Object.entries(message)) { - args.push(key, value); + parser.push(key, value); } - - return args; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( - key: RedisArgument, - id: RedisArgument, - message: Record, - options?: XAddOptions - ) { - return pushXAddArguments(['XADD', key], id, message, options); + parseCommand(...args: Tail>) { + return parseXAddArguments(undefined, ...args); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts index a3dd5602a3b..97927f212ff 100644 --- a/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XADD_NOMKSTREAM from './XADD_NOMKSTREAM'; +import { parseArgs } from './generic-transformers'; describe('XADD NOMKSTREAM', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,7 +9,7 @@ describe('XADD NOMKSTREAM', () => { describe('transformArguments', () => { it('single field', () => { assert.deepEqual( - XADD_NOMKSTREAM.transformArguments('key', '*', { + parseArgs(XADD_NOMKSTREAM, 'key', '*', { field: 'value' }), ['XADD', 'key', 'NOMKSTREAM', '*', 'field', 'value'] @@ -17,7 +18,7 @@ describe('XADD NOMKSTREAM', () => { it('multiple fields', () => { assert.deepEqual( - XADD_NOMKSTREAM.transformArguments('key', '*', { + parseArgs(XADD_NOMKSTREAM, 'key', '*', { '1': 'I', '2': 'II' }), @@ -27,7 +28,7 @@ describe('XADD NOMKSTREAM', () => { it('with TRIM', () => { assert.deepEqual( - XADD_NOMKSTREAM.transformArguments('key', '*', { + parseArgs(XADD_NOMKSTREAM, 'key', '*', { field: 'value' }, { TRIM: { @@ -40,7 +41,7 @@ describe('XADD NOMKSTREAM', () => { it('with TRIM.strategy', () => { assert.deepEqual( - XADD_NOMKSTREAM.transformArguments('key', '*', { + parseArgs(XADD_NOMKSTREAM, 'key', '*', { field: 'value' }, { TRIM: { @@ -54,7 +55,7 @@ describe('XADD NOMKSTREAM', () => { it('with TRIM.strategyModifier', () => { assert.deepEqual( - XADD_NOMKSTREAM.transformArguments('key', '*', { + parseArgs(XADD_NOMKSTREAM, 'key', '*', { field: 'value' }, { TRIM: { @@ -68,7 +69,7 @@ describe('XADD NOMKSTREAM', () => { it('with TRIM.limit', () => { assert.deepEqual( - XADD_NOMKSTREAM.transformArguments('key', '*', { + parseArgs(XADD_NOMKSTREAM, 'key', '*', { field: 'value' }, { TRIM: { diff --git a/packages/client/lib/commands/XADD_NOMKSTREAM.ts b/packages/client/lib/commands/XADD_NOMKSTREAM.ts index 65e7dd566e3..9d33374be4a 100644 --- a/packages/client/lib/commands/XADD_NOMKSTREAM.ts +++ b/packages/client/lib/commands/XADD_NOMKSTREAM.ts @@ -1,16 +1,11 @@ -import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; -import { XAddOptions, pushXAddArguments } from './XADD'; +import { BlobStringReply, NullReply, Command } from '../RESP/types'; +import { Tail } from './generic-transformers'; +import { parseXAddArguments } from './XADD'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( - key: RedisArgument, - id: RedisArgument, - message: Record, - options?: XAddOptions - ) { - return pushXAddArguments(['XADD', key, 'NOMKSTREAM'], id, message, options); + parseCommand(...args: Tail>) { + return parseXAddArguments('NOMKSTREAM', ...args); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XAUTOCLAIM.spec.ts b/packages/client/lib/commands/XAUTOCLAIM.spec.ts index 256c58cc4d6..58b09a63e78 100644 --- a/packages/client/lib/commands/XAUTOCLAIM.spec.ts +++ b/packages/client/lib/commands/XAUTOCLAIM.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XAUTOCLAIM from './XAUTOCLAIM'; +import { parseArgs } from './generic-transformers'; describe('XAUTOCLAIM', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,14 +9,14 @@ describe('XAUTOCLAIM', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - XAUTOCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0'), + parseArgs(XAUTOCLAIM, 'key', 'group', 'consumer', 1, '0-0'), ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0'] ); }); it('with COUNT', () => { assert.deepEqual( - XAUTOCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + parseArgs(XAUTOCLAIM, 'key', 'group', 'consumer', 1, '0-0', { COUNT: 1 }), ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'COUNT', '1'] diff --git a/packages/client/lib/commands/XAUTOCLAIM.ts b/packages/client/lib/commands/XAUTOCLAIM.ts index 7d33142de32..19b4f63a2df 100644 --- a/packages/client/lib/commands/XAUTOCLAIM.ts +++ b/packages/client/lib/commands/XAUTOCLAIM.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, TuplesReply, BlobStringReply, ArrayReply, NullReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; import { StreamMessageRawReply, transformStreamMessageNullReply } from './generic-transformers'; @@ -12,9 +13,9 @@ export type XAutoClaimRawReply = TuplesReply<[ ]>; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, group: RedisArgument, consumer: RedisArgument, @@ -22,20 +23,13 @@ export default { start: RedisArgument, options?: XAutoClaimOptions ) { - const args = [ - 'XAUTOCLAIM', - key, - group, - consumer, - minIdleTime.toString(), - start - ]; + parser.push('XAUTOCLAIM'); + parser.pushKey(key); + parser.push(group, consumer, minIdleTime.toString(), start); if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + parser.push('COUNT', options.COUNT.toString()); } - - return args; }, transformReply(reply: UnwrapReply, preserve?: any, typeMapping?: TypeMapping) { return { diff --git a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts index 96ceb1d8116..78911657086 100644 --- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts +++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XAUTOCLAIM_JUSTID from './XAUTOCLAIM_JUSTID'; +import { parseArgs } from './generic-transformers'; describe('XAUTOCLAIM JUSTID', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - XAUTOCLAIM_JUSTID.transformArguments('key', 'group', 'consumer', 1, '0-0'), + parseArgs(XAUTOCLAIM_JUSTID, 'key', 'group', 'consumer', 1, '0-0'), ['XAUTOCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID'] ); }); diff --git a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts index e2832f23536..c0ebe83748e 100644 --- a/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XAUTOCLAIM_JUSTID.ts @@ -8,12 +8,11 @@ type XAutoClaimJustIdRawReply = TuplesReply<[ ]>; export default { - FIRST_KEY_INDEX: XAUTOCLAIM.FIRST_KEY_INDEX, IS_READ_ONLY: XAUTOCLAIM.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = XAUTOCLAIM.transformArguments(...args); - redisArgs.push('JUSTID'); - return redisArgs; + parseCommand(...args: Parameters) { + const parser = args[0]; + XAUTOCLAIM.parseCommand(...args); + parser.push('JUSTID'); }, transformReply(reply: UnwrapReply) { return { diff --git a/packages/client/lib/commands/XCLAIM.spec.ts b/packages/client/lib/commands/XCLAIM.spec.ts index e8fde2e1a1a..90768509225 100644 --- a/packages/client/lib/commands/XCLAIM.spec.ts +++ b/packages/client/lib/commands/XCLAIM.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XCLAIM from './XCLAIM'; +import { parseArgs } from './generic-transformers'; describe('XCLAIM', () => { describe('transformArguments', () => { it('single id (string)', () => { assert.deepEqual( - XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0'), + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0'), ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0'] ); }); it('multiple ids (array)', () => { assert.deepEqual( - XCLAIM.transformArguments('key', 'group', 'consumer', 1, ['0-0', '1-0']), + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, ['0-0', '1-0']), ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', '1-0'] ); }); it('with IDLE', () => { assert.deepEqual( - XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', { IDLE: 1 }), ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'IDLE', '1'] @@ -30,7 +31,7 @@ describe('XCLAIM', () => { describe('with TIME', () => { it('number', () => { assert.deepEqual( - XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', { TIME: 1 }), ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', '1'] @@ -40,7 +41,7 @@ describe('XCLAIM', () => { it('Date', () => { const d = new Date(); assert.deepEqual( - XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', { TIME: d }), ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'TIME', d.getTime().toString()] @@ -50,7 +51,7 @@ describe('XCLAIM', () => { it('with RETRYCOUNT', () => { assert.deepEqual( - XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', { RETRYCOUNT: 1 }), ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'RETRYCOUNT', '1'] @@ -59,7 +60,7 @@ describe('XCLAIM', () => { it('with FORCE', () => { assert.deepEqual( - XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', { FORCE: true }), ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'FORCE'] @@ -68,7 +69,7 @@ describe('XCLAIM', () => { it('with LASTID', () => { assert.deepEqual( - XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', { LASTID: '0-0' }), ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'LASTID', '0-0'] @@ -77,7 +78,7 @@ describe('XCLAIM', () => { it('with IDLE, TIME, RETRYCOUNT, FORCE, LASTID', () => { assert.deepEqual( - XCLAIM.transformArguments('key', 'group', 'consumer', 1, '0-0', { + parseArgs(XCLAIM, 'key', 'group', 'consumer', 1, '0-0', { IDLE: 1, TIME: 1, RETRYCOUNT: 1, diff --git a/packages/client/lib/commands/XCLAIM.ts b/packages/client/lib/commands/XCLAIM.ts index eb9c0b325e1..598b1b17ba4 100644 --- a/packages/client/lib/commands/XCLAIM.ts +++ b/packages/client/lib/commands/XCLAIM.ts @@ -1,5 +1,6 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, NullReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments, StreamMessageRawReply, transformStreamMessageNullReply } from './generic-transformers'; +import { RedisVariadicArgument, StreamMessageRawReply, transformStreamMessageNullReply } from './generic-transformers'; export interface XClaimOptions { IDLE?: number; @@ -10,9 +11,9 @@ export interface XClaimOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, group: RedisArgument, consumer: RedisArgument, @@ -20,35 +21,33 @@ export default { id: RedisVariadicArgument, options?: XClaimOptions ) { - const args = pushVariadicArguments( - ['XCLAIM', key, group, consumer, minIdleTime.toString()], - id - ); + parser.push('XCLAIM'); + parser.pushKey(key); + parser.push(group, consumer, minIdleTime.toString()); + parser.pushVariadic(id); if (options?.IDLE !== undefined) { - args.push('IDLE', options.IDLE.toString()); + parser.push('IDLE', options.IDLE.toString()); } if (options?.TIME !== undefined) { - args.push( + parser.push( 'TIME', (options.TIME instanceof Date ? options.TIME.getTime() : options.TIME).toString() ); } if (options?.RETRYCOUNT !== undefined) { - args.push('RETRYCOUNT', options.RETRYCOUNT.toString()); + parser.push('RETRYCOUNT', options.RETRYCOUNT.toString()); } if (options?.FORCE) { - args.push('FORCE'); + parser.push('FORCE'); } if (options?.LASTID !== undefined) { - args.push('LASTID', options.LASTID); + parser.push('LASTID', options.LASTID); } - - return args; }, transformReply( reply: UnwrapReply>, diff --git a/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts b/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts index 6b580ac3c1b..d7bf9fdc70c 100644 --- a/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts +++ b/packages/client/lib/commands/XCLAIM_JUSTID.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XCLAIM_JUSTID from './XCLAIM_JUSTID'; +import { parseArgs } from './generic-transformers'; describe('XCLAIM JUSTID', () => { it('transformArguments', () => { assert.deepEqual( - XCLAIM_JUSTID.transformArguments('key', 'group', 'consumer', 1, '0-0'), + parseArgs(XCLAIM_JUSTID, 'key', 'group', 'consumer', 1, '0-0'), ['XCLAIM', 'key', 'group', 'consumer', '1', '0-0', 'JUSTID'] ); }); diff --git a/packages/client/lib/commands/XCLAIM_JUSTID.ts b/packages/client/lib/commands/XCLAIM_JUSTID.ts index 6200c9106e1..91be5aafbb4 100644 --- a/packages/client/lib/commands/XCLAIM_JUSTID.ts +++ b/packages/client/lib/commands/XCLAIM_JUSTID.ts @@ -2,12 +2,11 @@ import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; import XCLAIM from './XCLAIM'; export default { - FIRST_KEY_INDEX: XCLAIM.FIRST_KEY_INDEX, IS_READ_ONLY: XCLAIM.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = XCLAIM.transformArguments(...args); - redisArgs.push('JUSTID'); - return redisArgs; + parseCommand(...args: Parameters) { + const parser = args[0]; + XCLAIM.parseCommand(...args); + parser.push('JUSTID'); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XDEL.spec.ts b/packages/client/lib/commands/XDEL.spec.ts index 15875d3b7b3..510168bb765 100644 --- a/packages/client/lib/commands/XDEL.spec.ts +++ b/packages/client/lib/commands/XDEL.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XDEL from './XDEL'; +import { parseArgs } from './generic-transformers'; describe('XDEL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - XDEL.transformArguments('key', '0-0'), + parseArgs(XDEL, 'key', '0-0'), ['XDEL', 'key', '0-0'] ); }); it('array', () => { assert.deepEqual( - XDEL.transformArguments('key', ['0-0', '1-0']), + parseArgs(XDEL, 'key', ['0-0', '1-0']), ['XDEL', 'key', '0-0', '1-0'] ); }); diff --git a/packages/client/lib/commands/XDEL.ts b/packages/client/lib/commands/XDEL.ts index acc9198c2b8..ee385203ce5 100644 --- a/packages/client/lib/commands/XDEL.ts +++ b/packages/client/lib/commands/XDEL.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, id: RedisVariadicArgument) { - return pushVariadicArguments(['XDEL', key], id); + parseCommand(parser: CommandParser, key: RedisArgument, id: RedisVariadicArgument) { + parser.push('XDEL'); + parser.pushKey(key); + parser.pushVariadic(id); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XGROUP_CREATE.spec.ts b/packages/client/lib/commands/XGROUP_CREATE.spec.ts index 5c9071289c9..7c9d6298c6b 100644 --- a/packages/client/lib/commands/XGROUP_CREATE.spec.ts +++ b/packages/client/lib/commands/XGROUP_CREATE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XGROUP_CREATE from './XGROUP_CREATE'; +import { parseArgs } from './generic-transformers'; describe('XGROUP CREATE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - XGROUP_CREATE.transformArguments('key', 'group', '$'), + parseArgs(XGROUP_CREATE, 'key', 'group', '$'), ['XGROUP', 'CREATE', 'key', 'group', '$'] ); }); it('with MKSTREAM', () => { assert.deepEqual( - XGROUP_CREATE.transformArguments('key', 'group', '$', { + parseArgs(XGROUP_CREATE, 'key', 'group', '$', { MKSTREAM: true }), ['XGROUP', 'CREATE', 'key', 'group', '$', 'MKSTREAM'] @@ -22,7 +23,7 @@ describe('XGROUP CREATE', () => { it('with ENTRIESREAD', () => { assert.deepEqual( - XGROUP_CREATE.transformArguments('key', 'group', '$', { + parseArgs(XGROUP_CREATE, 'key', 'group', '$', { ENTRIESREAD: 1 }), ['XGROUP', 'CREATE', 'key', 'group', '$', 'ENTRIESREAD', '1'] diff --git a/packages/client/lib/commands/XGROUP_CREATE.ts b/packages/client/lib/commands/XGROUP_CREATE.ts index a04fcbeb044..e91186efe29 100644 --- a/packages/client/lib/commands/XGROUP_CREATE.ts +++ b/packages/client/lib/commands/XGROUP_CREATE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export interface XGroupCreateOptions { @@ -9,25 +10,25 @@ export interface XGroupCreateOptions { } export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, group: RedisArgument, id: RedisArgument, options?: XGroupCreateOptions ) { - const args = ['XGROUP', 'CREATE', key, group, id]; + parser.push('XGROUP', 'CREATE'); + parser.pushKey(key); + parser.push(group, id); if (options?.MKSTREAM) { - args.push('MKSTREAM'); + parser.push('MKSTREAM'); } if (options?.ENTRIESREAD) { - args.push('ENTRIESREAD', options.ENTRIESREAD.toString()); + parser.push('ENTRIESREAD', options.ENTRIESREAD.toString()); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts b/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts index 3c3ecbda0a7..eb749073d35 100644 --- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts +++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XGROUP_CREATECONSUMER from './XGROUP_CREATECONSUMER'; +import { parseArgs } from './generic-transformers'; describe('XGROUP CREATECONSUMER', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - XGROUP_CREATECONSUMER.transformArguments('key', 'group', 'consumer'), + parseArgs(XGROUP_CREATECONSUMER, 'key', 'group', 'consumer'), ['XGROUP', 'CREATECONSUMER', 'key', 'group', 'consumer'] ); }); diff --git a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts index 8fd21ca60de..906bc4c683e 100644 --- a/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_CREATECONSUMER.ts @@ -1,14 +1,17 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, Command, NumberReply } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, group: RedisArgument, consumer: RedisArgument ) { - return ['XGROUP', 'CREATECONSUMER', key, group, consumer]; + parser.push('XGROUP', 'CREATECONSUMER'); + parser.pushKey(key); + parser.push(group, consumer); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts b/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts index afc524eef86..fabef789d78 100644 --- a/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts +++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XGROUP_DELCONSUMER from './XGROUP_DELCONSUMER'; +import { parseArgs } from './generic-transformers'; describe('XGROUP DELCONSUMER', () => { it('transformArguments', () => { assert.deepEqual( - XGROUP_DELCONSUMER.transformArguments('key', 'group', 'consumer'), + parseArgs(XGROUP_DELCONSUMER, 'key', 'group', 'consumer'), ['XGROUP', 'DELCONSUMER', 'key', 'group', 'consumer'] ); }); diff --git a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts index 53007270e05..360d7e06cae 100644 --- a/packages/client/lib/commands/XGROUP_DELCONSUMER.ts +++ b/packages/client/lib/commands/XGROUP_DELCONSUMER.ts @@ -1,14 +1,17 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, group: RedisArgument, consumer: RedisArgument ) { - return ['XGROUP', 'DELCONSUMER', key, group, consumer]; + parser.push('XGROUP', 'DELCONSUMER'); + parser.pushKey(key); + parser.push(group, consumer); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XGROUP_DESTROY.spec.ts b/packages/client/lib/commands/XGROUP_DESTROY.spec.ts index 6ec90834518..8277c66d3f6 100644 --- a/packages/client/lib/commands/XGROUP_DESTROY.spec.ts +++ b/packages/client/lib/commands/XGROUP_DESTROY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XGROUP_DESTROY from './XGROUP_DESTROY'; +import { parseArgs } from './generic-transformers'; describe('XGROUP DESTROY', () => { it('transformArguments', () => { assert.deepEqual( - XGROUP_DESTROY.transformArguments('key', 'group'), + parseArgs(XGROUP_DESTROY, 'key', 'group'), ['XGROUP', 'DESTROY', 'key', 'group'] ); }); diff --git a/packages/client/lib/commands/XGROUP_DESTROY.ts b/packages/client/lib/commands/XGROUP_DESTROY.ts index 6c14d9ae2bd..9112f1bcd79 100644 --- a/packages/client/lib/commands/XGROUP_DESTROY.ts +++ b/packages/client/lib/commands/XGROUP_DESTROY.ts @@ -1,13 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: false, - transformArguments( - key: RedisArgument, - group: RedisArgument - ) { - return ['XGROUP', 'DESTROY', key, group]; + parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) { + parser.push('XGROUP', 'DESTROY'); + parser.pushKey(key); + parser.push(group); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XGROUP_SETID.spec.ts b/packages/client/lib/commands/XGROUP_SETID.spec.ts index 891a796d14d..6ea0dd79c37 100644 --- a/packages/client/lib/commands/XGROUP_SETID.spec.ts +++ b/packages/client/lib/commands/XGROUP_SETID.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XGROUP_SETID from './XGROUP_SETID'; +import { parseArgs } from './generic-transformers'; describe('XGROUP SETID', () => { it('transformArguments', () => { assert.deepEqual( - XGROUP_SETID.transformArguments('key', 'group', '0'), + parseArgs(XGROUP_SETID, 'key', 'group', '0'), ['XGROUP', 'SETID', 'key', 'group', '0'] ); }); diff --git a/packages/client/lib/commands/XGROUP_SETID.ts b/packages/client/lib/commands/XGROUP_SETID.ts index a23b4144335..5b0ddcc32ad 100644 --- a/packages/client/lib/commands/XGROUP_SETID.ts +++ b/packages/client/lib/commands/XGROUP_SETID.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export interface XGroupSetIdOptions { @@ -6,21 +7,21 @@ export interface XGroupSetIdOptions { } export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, group: RedisArgument, id: RedisArgument, options?: XGroupSetIdOptions ) { - const args = ['XGROUP', 'SETID', key, group, id]; + parser.push('XGROUP', 'SETID'); + parser.pushKey(key); + parser.push(group, id); if (options?.ENTRIESREAD) { - args.push('ENTRIESREAD', options.ENTRIESREAD.toString()); + parser.push('ENTRIESREAD', options.ENTRIESREAD.toString()); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts b/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts index 86abdbb1493..b1f245dbf18 100644 --- a/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts +++ b/packages/client/lib/commands/XINFO_CONSUMERS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XINFO_CONSUMERS from './XINFO_CONSUMERS'; +import { parseArgs } from './generic-transformers'; describe('XINFO CONSUMERS', () => { it('transformArguments', () => { assert.deepEqual( - XINFO_CONSUMERS.transformArguments('key', 'group'), + parseArgs(XINFO_CONSUMERS, 'key', 'group'), ['XINFO', 'CONSUMERS', 'key', 'group'] ); }); diff --git a/packages/client/lib/commands/XINFO_CONSUMERS.ts b/packages/client/lib/commands/XINFO_CONSUMERS.ts index ca0076d6335..310a40d17f3 100644 --- a/packages/client/lib/commands/XINFO_CONSUMERS.ts +++ b/packages/client/lib/commands/XINFO_CONSUMERS.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; export type XInfoConsumersReply = ArrayReply>; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments( - key: RedisArgument, - group: RedisArgument - ) { - return ['XINFO', 'CONSUMERS', key, group]; + parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) { + parser.push('XINFO', 'CONSUMERS'); + parser.pushKey(key); + parser.push(group); }, transformReply: { 2: (reply: UnwrapReply>) => { diff --git a/packages/client/lib/commands/XINFO_GROUPS.spec.ts b/packages/client/lib/commands/XINFO_GROUPS.spec.ts index 1bee02a0e6d..a1196f4957a 100644 --- a/packages/client/lib/commands/XINFO_GROUPS.spec.ts +++ b/packages/client/lib/commands/XINFO_GROUPS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XINFO_GROUPS from './XINFO_GROUPS'; +import { parseArgs } from './generic-transformers'; describe('XINFO GROUPS', () => { it('transformArguments', () => { assert.deepEqual( - XINFO_GROUPS.transformArguments('key'), + parseArgs(XINFO_GROUPS, 'key'), ['XINFO', 'GROUPS', 'key'] ); }); diff --git a/packages/client/lib/commands/XINFO_GROUPS.ts b/packages/client/lib/commands/XINFO_GROUPS.ts index 24661ecde84..e7f8874125a 100644 --- a/packages/client/lib/commands/XINFO_GROUPS.ts +++ b/packages/client/lib/commands/XINFO_GROUPS.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesToMapReply, BlobStringReply, NumberReply, NullReply, UnwrapReply, Resp2Reply, Command } from '../RESP/types'; export type XInfoGroupsReply = ArrayReply>; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['XINFO', 'GROUPS', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('XINFO', 'GROUPS'); + parser.pushKey(key); }, transformReply: { 2: (reply: UnwrapReply>) => { diff --git a/packages/client/lib/commands/XINFO_STREAM.spec.ts b/packages/client/lib/commands/XINFO_STREAM.spec.ts index 9e6939092e2..7e1829f3059 100644 --- a/packages/client/lib/commands/XINFO_STREAM.spec.ts +++ b/packages/client/lib/commands/XINFO_STREAM.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XINFO_STREAM from './XINFO_STREAM'; +import { parseArgs } from './generic-transformers'; describe('XINFO STREAM', () => { it('transformArguments', () => { assert.deepEqual( - XINFO_STREAM.transformArguments('key'), + parseArgs(XINFO_STREAM, 'key'), ['XINFO', 'STREAM', 'key'] ); }); diff --git a/packages/client/lib/commands/XINFO_STREAM.ts b/packages/client/lib/commands/XINFO_STREAM.ts index 04721d0ad32..bb102c591bb 100644 --- a/packages/client/lib/commands/XINFO_STREAM.ts +++ b/packages/client/lib/commands/XINFO_STREAM.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, TuplesToMapReply, BlobStringReply, NumberReply, NullReply, TuplesReply, ArrayReply, UnwrapReply, Command } from '../RESP/types'; import { isNullReply, transformTuplesReply } from './generic-transformers'; @@ -18,10 +19,10 @@ export type XInfoStreamReply = TuplesToMapReply<[ ]>; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['XINFO', 'STREAM', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('XINFO', 'STREAM'); + parser.pushKey(key); }, transformReply: { // TODO: is there a "type safe" way to do it? diff --git a/packages/client/lib/commands/XLEN.spec.ts b/packages/client/lib/commands/XLEN.spec.ts index 164a6d6f094..3e22b9aebfa 100644 --- a/packages/client/lib/commands/XLEN.spec.ts +++ b/packages/client/lib/commands/XLEN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XLEN from './XLEN'; +import { parseArgs } from './generic-transformers'; describe('XLEN', () => { - it('transformArguments', () => { + it('processCommand', () => { assert.deepEqual( - XLEN.transformArguments('key'), + parseArgs(XLEN, 'key'), ['XLEN', 'key'] ); }); diff --git a/packages/client/lib/commands/XLEN.ts b/packages/client/lib/commands/XLEN.ts index d2ed566a190..39d47187b28 100644 --- a/packages/client/lib/commands/XLEN.ts +++ b/packages/client/lib/commands/XLEN.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['XLEN', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('XLEN'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XPENDING.spec.ts b/packages/client/lib/commands/XPENDING.spec.ts index e6600cce383..55cb957fc62 100644 --- a/packages/client/lib/commands/XPENDING.spec.ts +++ b/packages/client/lib/commands/XPENDING.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XPENDING from './XPENDING'; +import { parseArgs } from './generic-transformers'; describe('XPENDING', () => { describe('transformArguments', () => { it('transformArguments', () => { assert.deepEqual( - XPENDING.transformArguments('key', 'group'), + parseArgs(XPENDING, 'key', 'group'), ['XPENDING', 'key', 'group'] ); }); diff --git a/packages/client/lib/commands/XPENDING.ts b/packages/client/lib/commands/XPENDING.ts index a6ca4f5a774..11c944c61e7 100644 --- a/packages/client/lib/commands/XPENDING.ts +++ b/packages/client/lib/commands/XPENDING.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, ArrayReply, TuplesReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; type XPendingRawReply = TuplesReply<[ @@ -11,10 +12,12 @@ type XPendingRawReply = TuplesReply<[ ]>; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, group: RedisArgument) { - return ['XPENDING', key, group]; + parseCommand(parser: CommandParser, key: RedisArgument, group: RedisArgument) { + parser.push('XPENDING'); + parser.pushKey(key); + parser.push(group); }, transformReply(reply: UnwrapReply) { const consumers = reply[3] as unknown as UnwrapReply; diff --git a/packages/client/lib/commands/XPENDING_RANGE.spec.ts b/packages/client/lib/commands/XPENDING_RANGE.spec.ts index a66484fd2e6..33cd836f2a9 100644 --- a/packages/client/lib/commands/XPENDING_RANGE.spec.ts +++ b/packages/client/lib/commands/XPENDING_RANGE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XPENDING_RANGE from './XPENDING_RANGE'; +import { parseArgs } from './generic-transformers'; describe('XPENDING RANGE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - XPENDING_RANGE.transformArguments('key', 'group', '-', '+', 1), + parseArgs(XPENDING_RANGE, 'key', 'group', '-', '+', 1), ['XPENDING', 'key', 'group', '-', '+', '1'] ); }); it('with IDLE', () => { assert.deepEqual( - XPENDING_RANGE.transformArguments('key', 'group', '-', '+', 1, { + parseArgs(XPENDING_RANGE, 'key', 'group', '-', '+', 1, { IDLE: 1, }), ['XPENDING', 'key', 'group', 'IDLE', '1', '-', '+', '1'] @@ -22,7 +23,7 @@ describe('XPENDING RANGE', () => { it('with consumer', () => { assert.deepEqual( - XPENDING_RANGE.transformArguments('key', 'group', '-', '+', 1, { + parseArgs(XPENDING_RANGE, 'key', 'group', '-', '+', 1, { consumer: 'consumer' }), ['XPENDING', 'key', 'group', '-', '+', '1', 'consumer'] @@ -31,7 +32,7 @@ describe('XPENDING RANGE', () => { it('with IDLE, consumer', () => { assert.deepEqual( - XPENDING_RANGE.transformArguments('key', 'group', '-', '+', 1, { + parseArgs(XPENDING_RANGE, 'key', 'group', '-', '+', 1, { IDLE: 1, consumer: 'consumer' }), diff --git a/packages/client/lib/commands/XPENDING_RANGE.ts b/packages/client/lib/commands/XPENDING_RANGE.ts index 60a28e5172d..8d98ffe7f1e 100644 --- a/packages/client/lib/commands/XPENDING_RANGE.ts +++ b/packages/client/lib/commands/XPENDING_RANGE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '../RESP/types'; export interface XPendingRangeOptions { @@ -13,33 +14,30 @@ type XPendingRangeRawReply = ArrayReply>; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, group: RedisArgument, start: RedisArgument, end: RedisArgument, count: number, options?: XPendingRangeOptions - ) { - const args = ['XPENDING', key, group]; + ) { + parser.push('XPENDING'); + parser.pushKey(key); + parser.push(group); if (options?.IDLE !== undefined) { - args.push('IDLE', options.IDLE.toString()); + parser.push('IDLE', options.IDLE.toString()); } - args.push( - start, - end, - count.toString() - ); + parser.push(start, end, count.toString()); if (options?.consumer) { - args.push(options.consumer); + parser.push(options.consumer); } - - return args; }, transformReply(reply: UnwrapReply) { return reply.map(pending => { diff --git a/packages/client/lib/commands/XRANGE.spec.ts b/packages/client/lib/commands/XRANGE.spec.ts index ebfe271db86..b111a97aff1 100644 --- a/packages/client/lib/commands/XRANGE.spec.ts +++ b/packages/client/lib/commands/XRANGE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XRANGE from './XRANGE'; +import { parseArgs } from './generic-transformers'; describe('XRANGE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - XRANGE.transformArguments('key', '-', '+'), + parseArgs(XRANGE, 'key', '-', '+'), ['XRANGE', 'key', '-', '+'] ); }); it('with COUNT', () => { assert.deepEqual( - XRANGE.transformArguments('key', '-', '+', { + parseArgs(XRANGE, 'key', '-', '+', { COUNT: 1 }), ['XRANGE', 'key', '-', '+', 'COUNT', '1'] diff --git a/packages/client/lib/commands/XRANGE.ts b/packages/client/lib/commands/XRANGE.ts index fb65160d810..de6bb6c9b1b 100644 --- a/packages/client/lib/commands/XRANGE.ts +++ b/packages/client/lib/commands/XRANGE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; import { StreamMessageRawReply, transformStreamMessageReply } from './generic-transformers'; @@ -5,14 +6,12 @@ export interface XRangeOptions { COUNT?: number; } -export function transformXRangeArguments( - command: RedisArgument, - key: RedisArgument, +export function xRangeArguments( start: RedisArgument, end: RedisArgument, options?: XRangeOptions ) { - const args = [command, key, start, end]; + const args = [start, end]; if (options?.COUNT) { args.push('COUNT', options.COUNT.toString()); @@ -22,9 +21,13 @@ export function transformXRangeArguments( } export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments: transformXRangeArguments.bind(undefined, 'XRANGE'), + parseCommand(parser: CommandParser, key: RedisArgument, ...args: Parameters) { + parser.push('XRANGE'); + parser.pushKey(key); + parser.pushVariadic(xRangeArguments(args[0], args[1], args[2])); + }, transformReply( reply: UnwrapReply>, preserve?: any, diff --git a/packages/client/lib/commands/XREAD.spec.ts b/packages/client/lib/commands/XREAD.spec.ts index 09784a56e6d..bb72c96497e 100644 --- a/packages/client/lib/commands/XREAD.spec.ts +++ b/packages/client/lib/commands/XREAD.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; +import testUtils, { GLOBAL, parseFirstKey } from '../test-utils'; import XREAD from './XREAD'; +import { parseArgs } from './generic-transformers'; describe('XREAD', () => { describe('FIRST_KEY_INDEX', () => { it('single stream', () => { assert.equal( - XREAD.FIRST_KEY_INDEX({ + parseFirstKey(XREAD, { key: 'key', id: '' }), @@ -16,7 +17,7 @@ describe('XREAD', () => { it('multiple streams', () => { assert.equal( - XREAD.FIRST_KEY_INDEX([{ + parseFirstKey(XREAD, [{ key: '1', id: '' }, { @@ -31,7 +32,7 @@ describe('XREAD', () => { describe('transformArguments', () => { it('single stream', () => { assert.deepEqual( - XREAD.transformArguments({ + parseArgs(XREAD, { key: 'key', id: '0-0' }), @@ -41,7 +42,7 @@ describe('XREAD', () => { it('multiple streams', () => { assert.deepEqual( - XREAD.transformArguments([{ + parseArgs(XREAD, [{ key: '1', id: '0-0' }, { @@ -54,7 +55,7 @@ describe('XREAD', () => { it('with COUNT', () => { assert.deepEqual( - XREAD.transformArguments({ + parseArgs(XREAD, { key: 'key', id: '0-0' }, { @@ -66,7 +67,7 @@ describe('XREAD', () => { it('with BLOCK', () => { assert.deepEqual( - XREAD.transformArguments({ + parseArgs(XREAD, { key: 'key', id: '0-0' }, { @@ -78,7 +79,7 @@ describe('XREAD', () => { it('with COUNT, BLOCK', () => { assert.deepEqual( - XREAD.transformArguments({ + parseArgs(XREAD, { key: 'key', id: '0-0' }, { @@ -90,7 +91,6 @@ describe('XREAD', () => { }); }); - testUtils.testAll('client.xRead', async client => { const message = { field: 'value' }, [id, reply] = await Promise.all([ diff --git a/packages/client/lib/commands/XREAD.ts b/packages/client/lib/commands/XREAD.ts index 97679376c19..b57fb8f3983 100644 --- a/packages/client/lib/commands/XREAD.ts +++ b/packages/client/lib/commands/XREAD.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { Command, RedisArgument, ReplyUnion } from '../RESP/types'; import { transformStreamsMessagesReplyResp2 } from './generic-transformers'; @@ -8,19 +9,19 @@ export interface XReadStream { export type XReadStreams = Array | XReadStream; -export function pushXReadStreams(args: Array, streams: XReadStreams) { - args.push('STREAMS'); +export function pushXReadStreams(parser: CommandParser, streams: XReadStreams) { + parser.push('STREAMS'); if (Array.isArray(streams)) { - const keysStart = args.length, - idsStart = keysStart + streams.length; for (let i = 0; i < streams.length; i++) { - const stream = streams[i]; - args[keysStart + i] = stream.key; - args[idsStart + i] = stream.id; + parser.pushKey(streams[i].key); + } + for (let i = 0; i < streams.length; i++) { + parser.push(streams[i].id); } } else { - args.push(streams.key, streams.id); + parser.pushKey(streams.key); + parser.push(streams.id); } } @@ -30,24 +31,19 @@ export interface XReadOptions { } export default { - FIRST_KEY_INDEX(streams: XReadStreams) { - return Array.isArray(streams) ? streams[0].key : streams.key; - }, IS_READ_ONLY: true, - transformArguments(streams: XReadStreams, options?: XReadOptions) { - const args: Array = ['XREAD']; + parseCommand(parser: CommandParser, streams: XReadStreams, options?: XReadOptions) { + parser.push('XREAD'); if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + parser.push('COUNT', options.COUNT.toString()); } if (options?.BLOCK !== undefined) { - args.push('BLOCK', options.BLOCK.toString()); + parser.push('BLOCK', options.BLOCK.toString()); } - pushXReadStreams(args, streams); - - return args; + pushXReadStreams(parser, streams); }, transformReply: { 2: transformStreamsMessagesReplyResp2, @@ -55,4 +51,3 @@ export default { }, unstableResp3: true } as const satisfies Command; - diff --git a/packages/client/lib/commands/XREADGROUP.spec.ts b/packages/client/lib/commands/XREADGROUP.spec.ts index 004a48ddbe3..085a67bc9b3 100644 --- a/packages/client/lib/commands/XREADGROUP.spec.ts +++ b/packages/client/lib/commands/XREADGROUP.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; +import testUtils, { GLOBAL, parseFirstKey } from '../test-utils'; import XREADGROUP from './XREADGROUP'; +import { parseArgs } from './generic-transformers'; describe('XREADGROUP', () => { describe('FIRST_KEY_INDEX', () => { it('single stream', () => { assert.equal( - XREADGROUP.FIRST_KEY_INDEX('', '', { key: 'key', id: '' }), + parseFirstKey(XREADGROUP, '', '', { key: 'key', id: '' }), 'key' ); }); it('multiple streams', () => { assert.equal( - XREADGROUP.FIRST_KEY_INDEX('', '', [{ key: '1', id: '' }, { key: '2', id: '' }]), + parseFirstKey(XREADGROUP, '', '', [{ key: '1', id: '' }, { key: '2', id: '' }]), '1' ); }); @@ -22,7 +23,7 @@ describe('XREADGROUP', () => { describe('transformArguments', () => { it('single stream', () => { assert.deepEqual( - XREADGROUP.transformArguments('group', 'consumer', { + parseArgs(XREADGROUP, 'group', 'consumer', { key: 'key', id: '0-0' }), @@ -32,7 +33,7 @@ describe('XREADGROUP', () => { it('multiple streams', () => { assert.deepEqual( - XREADGROUP.transformArguments('group', 'consumer', [{ + parseArgs(XREADGROUP, 'group', 'consumer', [{ key: '1', id: '0-0' }, { @@ -45,7 +46,7 @@ describe('XREADGROUP', () => { it('with COUNT', () => { assert.deepEqual( - XREADGROUP.transformArguments('group', 'consumer', { + parseArgs(XREADGROUP, 'group', 'consumer', { key: 'key', id: '0-0' }, { @@ -57,7 +58,7 @@ describe('XREADGROUP', () => { it('with BLOCK', () => { assert.deepEqual( - XREADGROUP.transformArguments('group', 'consumer', { + parseArgs(XREADGROUP, 'group', 'consumer', { key: 'key', id: '0-0' }, { @@ -69,7 +70,7 @@ describe('XREADGROUP', () => { it('with NOACK', () => { assert.deepEqual( - XREADGROUP.transformArguments('group', 'consumer', { + parseArgs(XREADGROUP, 'group', 'consumer', { key: 'key', id: '0-0' }, { @@ -81,7 +82,7 @@ describe('XREADGROUP', () => { it('with COUNT, BLOCK, NOACK', () => { assert.deepEqual( - XREADGROUP.transformArguments('group', 'consumer', { + parseArgs(XREADGROUP, 'group', 'consumer', { key: 'key', id: '0-0' }, { diff --git a/packages/client/lib/commands/XREADGROUP.ts b/packages/client/lib/commands/XREADGROUP.ts index 296480f9e3a..d0947e19a08 100644 --- a/packages/client/lib/commands/XREADGROUP.ts +++ b/packages/client/lib/commands/XREADGROUP.ts @@ -1,6 +1,7 @@ +import { CommandParser } from '../client/parser'; import { Command, RedisArgument, ReplyUnion } from '../RESP/types'; +import { XReadStreams, pushXReadStreams } from './XREAD'; import { transformStreamsMessagesReplyResp2 } from './generic-transformers'; -import XREAD, { XReadStreams, pushXReadStreams } from './XREAD'; export interface XReadGroupOptions { COUNT?: number; @@ -9,37 +10,29 @@ export interface XReadGroupOptions { } export default { - FIRST_KEY_INDEX( - _group: RedisArgument, - _consumer: RedisArgument, - streams: XReadStreams - ) { - return XREAD.FIRST_KEY_INDEX(streams); - }, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, group: RedisArgument, consumer: RedisArgument, streams: XReadStreams, options?: XReadGroupOptions ) { - const args = ['XREADGROUP', 'GROUP', group, consumer]; + parser.push('XREADGROUP', 'GROUP', group, consumer); if (options?.COUNT !== undefined) { - args.push('COUNT', options.COUNT.toString()); + parser.push('COUNT', options.COUNT.toString()); } if (options?.BLOCK !== undefined) { - args.push('BLOCK', options.BLOCK.toString()); + parser.push('BLOCK', options.BLOCK.toString()); } if (options?.NOACK) { - args.push('NOACK'); + parser.push('NOACK'); } - pushXReadStreams(args, streams); - - return args; + pushXReadStreams(parser, streams); }, transformReply: { 2: transformStreamsMessagesReplyResp2, diff --git a/packages/client/lib/commands/XREVRANGE.spec.ts b/packages/client/lib/commands/XREVRANGE.spec.ts index c9f3043af38..9872dc5e9e0 100644 --- a/packages/client/lib/commands/XREVRANGE.spec.ts +++ b/packages/client/lib/commands/XREVRANGE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XREVRANGE from './XREVRANGE'; +import { parseArgs } from './generic-transformers'; describe('XREVRANGE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - XREVRANGE.transformArguments('key', '-', '+'), + parseArgs(XREVRANGE, 'key', '-', '+'), ['XREVRANGE', 'key', '-', '+'] ); }); it('with COUNT', () => { assert.deepEqual( - XREVRANGE.transformArguments('key', '-', '+', { + parseArgs(XREVRANGE, 'key', '-', '+', { COUNT: 1 }), ['XREVRANGE', 'key', '-', '+', 'COUNT', '1'] diff --git a/packages/client/lib/commands/XREVRANGE.ts b/packages/client/lib/commands/XREVRANGE.ts index 86e4d8abc9e..ddc51082a1e 100644 --- a/packages/client/lib/commands/XREVRANGE.ts +++ b/packages/client/lib/commands/XREVRANGE.ts @@ -1,13 +1,18 @@ -import { Command } from '../RESP/types'; -import XRANGE, { transformXRangeArguments } from './XRANGE'; +import { CommandParser } from '../client/parser'; +import { Command, RedisArgument } from '../RESP/types'; +import XRANGE, { xRangeArguments } from './XRANGE'; export interface XRevRangeOptions { COUNT?: number; } export default { - FIRST_KEY_INDEX: XRANGE.FIRST_KEY_INDEX, + CACHEABLE: XRANGE.CACHEABLE, IS_READ_ONLY: XRANGE.IS_READ_ONLY, - transformArguments: transformXRangeArguments.bind(undefined, 'XREVRANGE'), + parseCommand(parser: CommandParser, key: RedisArgument, ...args: Parameters) { + parser.push('XREVRANGE'); + parser.pushKey(key); + parser.pushVariadic(xRangeArguments(args[0], args[1], args[2])); + }, transformReply: XRANGE.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/XSETID.spec.ts b/packages/client/lib/commands/XSETID.spec.ts index 5ebbc943816..b3609695345 100644 --- a/packages/client/lib/commands/XSETID.spec.ts +++ b/packages/client/lib/commands/XSETID.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XSETID from './XSETID'; +import { parseArgs } from './generic-transformers'; describe('XSETID', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - XSETID.transformArguments('key', '0-0'), + parseArgs(XSETID, 'key', '0-0'), ['XSETID', 'key', '0-0'] ); }); it('with ENTRIESADDED', () => { assert.deepEqual( - XSETID.transformArguments('key', '0-0', { + parseArgs(XSETID, 'key', '0-0', { ENTRIESADDED: 1 }), ['XSETID', 'key', '0-0', 'ENTRIESADDED', '1'] @@ -22,7 +23,7 @@ describe('XSETID', () => { it('with MAXDELETEDID', () => { assert.deepEqual( - XSETID.transformArguments('key', '0-0', { + parseArgs(XSETID, 'key', '0-0', { MAXDELETEDID: '1-1' }), ['XSETID', 'key', '0-0', 'MAXDELETEDID', '1-1'] diff --git a/packages/client/lib/commands/XSETID.ts b/packages/client/lib/commands/XSETID.ts index a2ac8af0011..c76ac0b23a4 100644 --- a/packages/client/lib/commands/XSETID.ts +++ b/packages/client/lib/commands/XSETID.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../RESP/types'; export interface XSetIdOptions { /** added in 7.0 */ @@ -7,24 +8,24 @@ export interface XSetIdOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, lastId: RedisArgument, options?: XSetIdOptions ) { - const args = ['XSETID', key, lastId]; + parser.push('XSETID'); + parser.pushKey(key); + parser.push(lastId); if (options?.ENTRIESADDED) { - args.push('ENTRIESADDED', options.ENTRIESADDED.toString()); + parser.push('ENTRIESADDED', options.ENTRIESADDED.toString()); } if (options?.MAXDELETEDID) { - args.push('MAXDELETEDID', options.MAXDELETEDID); + parser.push('MAXDELETEDID', options.MAXDELETEDID); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/commands/XTRIM.spec.ts b/packages/client/lib/commands/XTRIM.spec.ts index a5a2fdf23c5..2c31f0fef92 100644 --- a/packages/client/lib/commands/XTRIM.spec.ts +++ b/packages/client/lib/commands/XTRIM.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import XTRIM from './XTRIM'; +import { parseArgs } from './generic-transformers'; describe('XTRIM', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - XTRIM.transformArguments('key', 'MAXLEN', 1), + parseArgs(XTRIM, 'key', 'MAXLEN', 1), ['XTRIM', 'key', 'MAXLEN', '1'] ); }); it('with strategyModifier', () => { assert.deepEqual( - XTRIM.transformArguments('key', 'MAXLEN', 1, { + parseArgs(XTRIM, 'key', 'MAXLEN', 1, { strategyModifier: '=' }), ['XTRIM', 'key', 'MAXLEN', '=', '1'] @@ -22,7 +23,7 @@ describe('XTRIM', () => { it('with LIMIT', () => { assert.deepEqual( - XTRIM.transformArguments('key', 'MAXLEN', 1, { + parseArgs(XTRIM, 'key', 'MAXLEN', 1, { LIMIT: 1 }), ['XTRIM', 'key', 'MAXLEN', '1', 'LIMIT', '1'] @@ -31,7 +32,7 @@ describe('XTRIM', () => { it('with strategyModifier, LIMIT', () => { assert.deepEqual( - XTRIM.transformArguments('key', 'MAXLEN', 1, { + parseArgs(XTRIM, 'key', 'MAXLEN', 1, { strategyModifier: '=', LIMIT: 1 }), diff --git a/packages/client/lib/commands/XTRIM.ts b/packages/client/lib/commands/XTRIM.ts index 0512323a32a..fb617d8d35a 100644 --- a/packages/client/lib/commands/XTRIM.ts +++ b/packages/client/lib/commands/XTRIM.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; export interface XTrimOptions { @@ -7,27 +8,27 @@ export interface XTrimOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, strategy: 'MAXLEN' | 'MINID', threshold: number, options?: XTrimOptions ) { - const args = ['XTRIM', key, strategy]; + parser.push('XTRIM') + parser.pushKey(key); + parser.push(strategy); if (options?.strategyModifier) { - args.push(options.strategyModifier); + parser.push(options.strategyModifier); } - args.push(threshold.toString()); + parser.push(threshold.toString()); if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.toString()); + parser.push('LIMIT', options.LIMIT.toString()); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZADD.spec.ts b/packages/client/lib/commands/ZADD.spec.ts index 5f64cb13b92..0e770693e3a 100644 --- a/packages/client/lib/commands/ZADD.spec.ts +++ b/packages/client/lib/commands/ZADD.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZADD from './ZADD'; +import { parseArgs } from './generic-transformers'; describe('ZADD', () => { describe('transformArguments', () => { it('single member', () => { assert.deepEqual( - ZADD.transformArguments('key', { + parseArgs(ZADD, 'key', { value: '1', score: 1 }), @@ -16,7 +17,7 @@ describe('ZADD', () => { it('multiple members', () => { assert.deepEqual( - ZADD.transformArguments('key', [{ + parseArgs(ZADD, 'key', [{ value: '1', score: 1 }, { @@ -30,7 +31,7 @@ describe('ZADD', () => { describe('with condition', () => { it('condition property', () => { assert.deepEqual( - ZADD.transformArguments('key', { + parseArgs(ZADD, 'key', { value: '1', score: 1 }, { @@ -42,7 +43,7 @@ describe('ZADD', () => { it('with NX (backwards compatibility)', () => { assert.deepEqual( - ZADD.transformArguments('key', { + parseArgs(ZADD, 'key', { value: '1', score: 1 }, { @@ -54,7 +55,7 @@ describe('ZADD', () => { it('with XX (backwards compatibility)', () => { assert.deepEqual( - ZADD.transformArguments('key', { + parseArgs(ZADD, 'key', { value: '1', score: 1 }, { @@ -68,7 +69,7 @@ describe('ZADD', () => { describe('with comparison', () => { it('with LT', () => { assert.deepEqual( - ZADD.transformArguments('key', { + parseArgs(ZADD, 'key', { value: '1', score: 1 }, { @@ -80,7 +81,7 @@ describe('ZADD', () => { it('with LT (backwards compatibility)', () => { assert.deepEqual( - ZADD.transformArguments('key', { + parseArgs(ZADD, 'key', { value: '1', score: 1 }, { @@ -92,7 +93,7 @@ describe('ZADD', () => { it('with GT (backwards compatibility)', () => { assert.deepEqual( - ZADD.transformArguments('key', { + parseArgs(ZADD, 'key', { value: '1', score: 1 }, { @@ -105,7 +106,7 @@ describe('ZADD', () => { it('with CH', () => { assert.deepEqual( - ZADD.transformArguments('key', { + parseArgs(ZADD, 'key', { value: '1', score: 1 }, { @@ -117,7 +118,7 @@ describe('ZADD', () => { it('with condition, comparison, CH', () => { assert.deepEqual( - ZADD.transformArguments('key', { + parseArgs(ZADD, 'key', { value: '1', score: 1 }, { diff --git a/packages/client/lib/commands/ZADD.ts b/packages/client/lib/commands/ZADD.ts index 0c5602bf988..5ae71a151ba 100644 --- a/packages/client/lib/commands/ZADD.ts +++ b/packages/client/lib/commands/ZADD.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, Command } from '../RESP/types'; import { SortedSetMember, transformDoubleArgument, transformDoubleReply } from './generic-transformers'; @@ -24,58 +25,57 @@ export interface ZAddOptions { } export default { - FIRST_KEY_INDEX: 1, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, members: SortedSetMember | Array, options?: ZAddOptions ) { - const args = ['ZADD', key]; + parser.push('ZADD'); + parser.pushKey(key); if (options?.condition) { - args.push(options.condition); + parser.push(options.condition); } else if (options?.NX) { - args.push('NX'); + parser.push('NX'); } else if (options?.XX) { - args.push('XX'); + parser.push('XX'); } if (options?.comparison) { - args.push(options.comparison); + parser.push(options.comparison); } else if (options?.LT) { - args.push('LT'); + parser.push('LT'); } else if (options?.GT) { - args.push('GT'); + parser.push('GT'); } if (options?.CH) { - args.push('CH'); + parser.push('CH'); } - pushMembers(args, members); - - return args; + pushMembers(parser, members); }, transformReply: transformDoubleReply } as const satisfies Command; export function pushMembers( - args: Array, + parser: CommandParser, members: SortedSetMember | Array) { if (Array.isArray(members)) { for (const member of members) { - pushMember(args, member); + pushMember(parser, member); } } else { - pushMember(args, members); + pushMember(parser, members); } } function pushMember( - args: Array, + parser: CommandParser, member: SortedSetMember ) { - args.push( + parser.push( transformDoubleArgument(member.score), member.value ); diff --git a/packages/client/lib/commands/ZADD_INCR.spec.ts b/packages/client/lib/commands/ZADD_INCR.spec.ts index c6ffcb796f3..df9ac87f449 100644 --- a/packages/client/lib/commands/ZADD_INCR.spec.ts +++ b/packages/client/lib/commands/ZADD_INCR.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZADD_INCR from './ZADD_INCR'; +import { parseArgs } from './generic-transformers'; describe('ZADD INCR', () => { describe('transformArguments', () => { it('single member', () => { assert.deepEqual( - ZADD_INCR.transformArguments('key', { + parseArgs(ZADD_INCR, 'key', { value: '1', score: 1 }), @@ -16,7 +17,7 @@ describe('ZADD INCR', () => { it('multiple members', () => { assert.deepEqual( - ZADD_INCR.transformArguments('key', [{ + parseArgs(ZADD_INCR, 'key', [{ value: '1', score: 1 }, { @@ -29,7 +30,7 @@ describe('ZADD INCR', () => { it('with condition', () => { assert.deepEqual( - ZADD_INCR.transformArguments('key', { + parseArgs(ZADD_INCR, 'key', { value: '1', score: 1 }, { @@ -41,7 +42,7 @@ describe('ZADD INCR', () => { it('with comparison', () => { assert.deepEqual( - ZADD_INCR.transformArguments('key', { + parseArgs(ZADD_INCR, 'key', { value: '1', score: 1 }, { @@ -53,7 +54,7 @@ describe('ZADD INCR', () => { it('with CH', () => { assert.deepEqual( - ZADD_INCR.transformArguments('key', { + parseArgs(ZADD_INCR, 'key', { value: '1', score: 1 }, { @@ -65,7 +66,7 @@ describe('ZADD INCR', () => { it('with condition, comparison, CH', () => { assert.deepEqual( - ZADD_INCR.transformArguments('key', { + parseArgs(ZADD_INCR, 'key', { value: '1', score: 1 }, { diff --git a/packages/client/lib/commands/ZADD_INCR.ts b/packages/client/lib/commands/ZADD_INCR.ts index 8fb10721674..f37554b1681 100644 --- a/packages/client/lib/commands/ZADD_INCR.ts +++ b/packages/client/lib/commands/ZADD_INCR.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, Command } from '../RESP/types'; import { pushMembers } from './ZADD'; import { SortedSetMember, transformNullableDoubleReply } from './generic-transformers'; @@ -9,31 +10,30 @@ export interface ZAddOptions { } export default { - FIRST_KEY_INDEX: 1, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, members: SortedSetMember | Array, options?: ZAddOptions ) { - const args = ['ZADD', key]; + parser.push('ZADD'); + parser.pushKey(key); if (options?.condition) { - args.push(options.condition); + parser.push(options.condition); } if (options?.comparison) { - args.push(options.comparison); + parser.push(options.comparison); } if (options?.CH) { - args.push('CH'); + parser.push('CH'); } - args.push('INCR'); + parser.push('INCR'); - pushMembers(args, members); - - return args; + pushMembers(parser, members); }, transformReply: transformNullableDoubleReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZCARD.spec.ts b/packages/client/lib/commands/ZCARD.spec.ts index ff4bb881fb7..44adec0833a 100644 --- a/packages/client/lib/commands/ZCARD.spec.ts +++ b/packages/client/lib/commands/ZCARD.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZCARD from './ZCARD'; +import { parseArgs } from './generic-transformers'; describe('ZCARD', () => { it('transformArguments', () => { assert.deepEqual( - ZCARD.transformArguments('key'), + parseArgs(ZCARD, 'key'), ['ZCARD', 'key'] ); }); diff --git a/packages/client/lib/commands/ZCARD.ts b/packages/client/lib/commands/ZCARD.ts index c11cb69db3c..57b9e7f1d47 100644 --- a/packages/client/lib/commands/ZCARD.ts +++ b/packages/client/lib/commands/ZCARD.ts @@ -1,10 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['ZCARD', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('ZCARD'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZCOUNT.spec.ts b/packages/client/lib/commands/ZCOUNT.spec.ts index 357f24bd796..5d279d7a4ca 100644 --- a/packages/client/lib/commands/ZCOUNT.spec.ts +++ b/packages/client/lib/commands/ZCOUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZCOUNT from './ZCOUNT'; +import { parseArgs } from './generic-transformers'; describe('ZCOUNT', () => { it('transformArguments', () => { assert.deepEqual( - ZCOUNT.transformArguments('key', 0, 1), + parseArgs(ZCOUNT, 'key', 0, 1), ['ZCOUNT', 'key', '0', '1'] ); }); diff --git a/packages/client/lib/commands/ZCOUNT.ts b/packages/client/lib/commands/ZCOUNT.ts index 187a316b15a..ccbc3d13d9b 100644 --- a/packages/client/lib/commands/ZCOUNT.ts +++ b/packages/client/lib/commands/ZCOUNT.ts @@ -1,20 +1,22 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, min: number | RedisArgument, max: number | RedisArgument ) { - return [ - 'ZCOUNT', - key, - transformStringDoubleArgument(min), + parser.push('ZCOUNT'); + parser.pushKey(key); + parser.push( + transformStringDoubleArgument(min), transformStringDoubleArgument(max) - ]; + ); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZDIFF.spec.ts b/packages/client/lib/commands/ZDIFF.spec.ts index a285190398c..4914df3e978 100644 --- a/packages/client/lib/commands/ZDIFF.spec.ts +++ b/packages/client/lib/commands/ZDIFF.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZDIFF from './ZDIFF'; +import { parseArgs } from './generic-transformers'; describe('ZDIFF', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,14 +9,14 @@ describe('ZDIFF', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - ZDIFF.transformArguments('key'), + parseArgs(ZDIFF, 'key'), ['ZDIFF', '1', 'key'] ); }); it('array', () => { assert.deepEqual( - ZDIFF.transformArguments(['1', '2']), + parseArgs(ZDIFF, ['1', '2']), ['ZDIFF', '2', '1', '2'] ); }); diff --git a/packages/client/lib/commands/ZDIFF.ts b/packages/client/lib/commands/ZDIFF.ts index f16c8717cdb..28135dc9c13 100644 --- a/packages/client/lib/commands/ZDIFF.ts +++ b/packages/client/lib/commands/ZDIFF.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments(keys: RedisVariadicArgument) { - return pushVariadicArgument(['ZDIFF'], keys); + parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { + parser.push('ZDIFF'); + parser.pushKeysLength(keys); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZDIFFSTORE.spec.ts b/packages/client/lib/commands/ZDIFFSTORE.spec.ts index 1bd779302bf..7f380cfc532 100644 --- a/packages/client/lib/commands/ZDIFFSTORE.spec.ts +++ b/packages/client/lib/commands/ZDIFFSTORE.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZDIFFSTORE from './ZDIFFSTORE'; +import { parseArgs } from './generic-transformers'; describe('ZDIFFSTORE', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,14 +9,14 @@ describe('ZDIFFSTORE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - ZDIFFSTORE.transformArguments('destination', 'key'), + parseArgs(ZDIFFSTORE, 'destination', 'key'), ['ZDIFFSTORE', 'destination', '1', 'key'] ); }); it('array', () => { assert.deepEqual( - ZDIFFSTORE.transformArguments('destination', ['1', '2']), + parseArgs(ZDIFFSTORE, 'destination', ['1', '2']), ['ZDIFFSTORE', 'destination', '2', '1', '2'] ); }); diff --git a/packages/client/lib/commands/ZDIFFSTORE.ts b/packages/client/lib/commands/ZDIFFSTORE.ts index e4614a1cb14..d83a4bdc851 100644 --- a/packages/client/lib/commands/ZDIFFSTORE.ts +++ b/packages/client/lib/commands/ZDIFFSTORE.ts @@ -1,14 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments( - destination: RedisArgument, - inputKeys: RedisVariadicArgument - ) { - return pushVariadicArgument(['ZDIFFSTORE', destination], inputKeys); + parseCommand(parser: CommandParser, destination: RedisArgument, inputKeys: RedisVariadicArgument) { + parser.push('ZDIFFSTORE'); + parser.pushKey(destination); + parser.pushKeysLength(inputKeys); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts b/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts index 4fcd1f978a3..bea639f223e 100644 --- a/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZDIFF_WITHSCORES.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZDIFF_WITHSCORES from './ZDIFF_WITHSCORES'; +import { parseArgs } from './generic-transformers'; describe('ZDIFF WITHSCORES', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,14 +9,14 @@ describe('ZDIFF WITHSCORES', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - ZDIFF_WITHSCORES.transformArguments('key'), + parseArgs(ZDIFF_WITHSCORES, 'key'), ['ZDIFF', '1', 'key', 'WITHSCORES'] ); }); it('array', () => { assert.deepEqual( - ZDIFF_WITHSCORES.transformArguments(['1', '2']), + parseArgs(ZDIFF_WITHSCORES, ['1', '2']), ['ZDIFF', '2', '1', '2', 'WITHSCORES'] ); }); diff --git a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts index f971e828574..4088f106dc6 100644 --- a/packages/client/lib/commands/ZDIFF_WITHSCORES.ts +++ b/packages/client/lib/commands/ZDIFF_WITHSCORES.ts @@ -1,14 +1,14 @@ +import { CommandParser } from '../client/parser'; import { Command } from '../RESP/types'; +import { RedisVariadicArgument, transformSortedSetReply } from './generic-transformers'; import ZDIFF from './ZDIFF'; -import { transformSortedSetReply } from './generic-transformers'; + export default { - FIRST_KEY_INDEX: ZDIFF.FIRST_KEY_INDEX, IS_READ_ONLY: ZDIFF.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = ZDIFF.transformArguments(...args); - redisArgs.push('WITHSCORES'); - return redisArgs; + parseCommand(parser: CommandParser, keys: RedisVariadicArgument) { + ZDIFF.parseCommand(parser, keys); + parser.push('WITHSCORES'); }, transformReply: transformSortedSetReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZINCRBY.spec.ts b/packages/client/lib/commands/ZINCRBY.spec.ts index fea3c7a2f71..8f6c5141252 100644 --- a/packages/client/lib/commands/ZINCRBY.spec.ts +++ b/packages/client/lib/commands/ZINCRBY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZINCRBY from './ZINCRBY'; +import { parseArgs } from './generic-transformers'; describe('ZINCRBY', () => { it('transformArguments', () => { assert.deepEqual( - ZINCRBY.transformArguments('key', 1, 'member'), + parseArgs(ZINCRBY, 'key', 1, 'member'), ['ZINCRBY', 'key', '1', 'member'] ); }); diff --git a/packages/client/lib/commands/ZINCRBY.ts b/packages/client/lib/commands/ZINCRBY.ts index d9e43845016..5e461891e9c 100644 --- a/packages/client/lib/commands/ZINCRBY.ts +++ b/packages/client/lib/commands/ZINCRBY.ts @@ -1,19 +1,17 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, Command } from '../RESP/types'; import { transformDoubleArgument, transformDoubleReply } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, increment: number, member: RedisArgument ) { - return [ - 'ZINCRBY', - key, - transformDoubleArgument(increment), - member - ]; + parser.push('ZINCRBY'); + parser.pushKey(key); + parser.push(transformDoubleArgument(increment), member); }, transformReply: transformDoubleReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZINTER.spec.ts b/packages/client/lib/commands/ZINTER.spec.ts index 0d678ce1151..73df0935de9 100644 --- a/packages/client/lib/commands/ZINTER.spec.ts +++ b/packages/client/lib/commands/ZINTER.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZINTER from './ZINTER'; +import { parseArgs } from './generic-transformers'; describe('ZINTER', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,21 +9,21 @@ describe('ZINTER', () => { describe('transformArguments', () => { it('key (string)', () => { assert.deepEqual( - ZINTER.transformArguments('key'), + parseArgs(ZINTER, 'key'), ['ZINTER', '1', 'key'] ); }); it('keys (Array)', () => { assert.deepEqual( - ZINTER.transformArguments(['1', '2']), + parseArgs(ZINTER, ['1', '2']), ['ZINTER', '2', '1', '2'] ); }); it('key & weight', () => { assert.deepEqual( - ZINTER.transformArguments({ + parseArgs(ZINTER, { key: 'key', weight: 1 }), @@ -32,7 +33,7 @@ describe('ZINTER', () => { it('keys & weights', () => { assert.deepEqual( - ZINTER.transformArguments([{ + parseArgs(ZINTER, [{ key: 'a', weight: 1 }, { @@ -45,7 +46,7 @@ describe('ZINTER', () => { it('with AGGREGATE', () => { assert.deepEqual( - ZINTER.transformArguments('key', { + parseArgs(ZINTER, 'key', { AGGREGATE: 'SUM' }), ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM'] diff --git a/packages/client/lib/commands/ZINTER.ts b/packages/client/lib/commands/ZINTER.ts index 392c3a96c65..740d3c295ec 100644 --- a/packages/client/lib/commands/ZINTER.ts +++ b/packages/client/lib/commands/ZINTER.ts @@ -1,5 +1,6 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { ZKeys, pushZKeysArguments } from './generic-transformers'; +import { ZKeys, parseZKeysArguments } from './generic-transformers'; export type ZInterKeyAndWeight = { key: RedisArgument; @@ -8,32 +9,29 @@ export type ZInterKeyAndWeight = { export type ZInterKeys = T | [T, ...Array]; +export type ZInterKeysType = ZInterKeys | ZInterKeys; + export interface ZInterOptions { AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; } -export function pushZInterArguments( - args: Array, +export function parseZInterArguments( + parser: CommandParser, keys: ZKeys, options?: ZInterOptions ) { - args = pushZKeysArguments(args, keys); + parseZKeysArguments(parser, keys); if (options?.AGGREGATE) { - args.push('AGGREGATE', options.AGGREGATE); + parser.push('AGGREGATE', options.AGGREGATE); } - - return args; } export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments( - keys: ZInterKeys | ZInterKeys, - options?: ZInterOptions - ) { - return pushZInterArguments(['ZINTER'], keys, options); + parseCommand(parser: CommandParser, keys: ZInterKeysType, options?: ZInterOptions) { + parser.push('ZINTER'); + parseZInterArguments(parser, keys, options); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZINTERCARD.spec.ts b/packages/client/lib/commands/ZINTERCARD.spec.ts index 312e2825ff4..5204872a2d0 100644 --- a/packages/client/lib/commands/ZINTERCARD.spec.ts +++ b/packages/client/lib/commands/ZINTERCARD.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZINTERCARD from './ZINTERCARD'; +import { parseArgs } from './generic-transformers'; describe('ZINTERCARD', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,7 +9,7 @@ describe('ZINTERCARD', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ZINTERCARD.transformArguments(['1', '2']), + parseArgs(ZINTERCARD, ['1', '2']), ['ZINTERCARD', '2', '1', '2'] ); }); @@ -16,14 +17,14 @@ describe('ZINTERCARD', () => { describe('with LIMIT', () => { it('plain number (backwards compatibility)', () => { assert.deepEqual( - ZINTERCARD.transformArguments(['1', '2'], 1), + parseArgs(ZINTERCARD, ['1', '2'], 1), ['ZINTERCARD', '2', '1', '2', 'LIMIT', '1'] ); }); it('{ LIMIT: number }', () => { assert.deepEqual( - ZINTERCARD.transformArguments(['1', '2'], { + parseArgs(ZINTERCARD, ['1', '2'], { LIMIT: 1 }), ['ZINTERCARD', '2', '1', '2', 'LIMIT', '1'] diff --git a/packages/client/lib/commands/ZINTERCARD.ts b/packages/client/lib/commands/ZINTERCARD.ts index 9953d88eecc..8c2e98d12cb 100644 --- a/packages/client/lib/commands/ZINTERCARD.ts +++ b/packages/client/lib/commands/ZINTERCARD.ts @@ -1,27 +1,27 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArgument } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export interface ZInterCardOptions { LIMIT?: number; } export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, keys: RedisVariadicArgument, options?: ZInterCardOptions['LIMIT'] | ZInterCardOptions ) { - const args = pushVariadicArgument(['ZINTERCARD'], keys); + parser.push('ZINTERCARD'); + parser.pushKeysLength(keys); // backwards compatibility if (typeof options === 'number') { - args.push('LIMIT', options.toString()); + parser.push('LIMIT', options.toString()); } else if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.toString()); + parser.push('LIMIT', options.LIMIT.toString()); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZINTERSTORE.spec.ts b/packages/client/lib/commands/ZINTERSTORE.spec.ts index 27914ca0327..c6b448ab908 100644 --- a/packages/client/lib/commands/ZINTERSTORE.spec.ts +++ b/packages/client/lib/commands/ZINTERSTORE.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZINTERSTORE from './ZINTERSTORE'; +import { parseArgs } from './generic-transformers'; describe('ZINTERSTORE', () => { describe('transformArguments', () => { it('key (string)', () => { assert.deepEqual( - ZINTERSTORE.transformArguments('destination', 'source'), + parseArgs(ZINTERSTORE, 'destination', 'source'), ['ZINTERSTORE', 'destination', '1', 'source'] ); }); it('keys (Array)', () => { assert.deepEqual( - ZINTERSTORE.transformArguments('destination', ['1', '2']), + parseArgs(ZINTERSTORE, 'destination', ['1', '2']), ['ZINTERSTORE', 'destination', '2', '1', '2'] ); }); it('key & weight', () => { assert.deepEqual( - ZINTERSTORE.transformArguments('destination', { + parseArgs(ZINTERSTORE, 'destination', { key: 'source', weight: 1 }), @@ -30,7 +31,7 @@ describe('ZINTERSTORE', () => { it('keys & weights', () => { assert.deepEqual( - ZINTERSTORE.transformArguments('destination', [{ + parseArgs(ZINTERSTORE, 'destination', [{ key: 'a', weight: 1 }, { @@ -43,7 +44,7 @@ describe('ZINTERSTORE', () => { it('with AGGREGATE', () => { assert.deepEqual( - ZINTERSTORE.transformArguments('destination', 'source', { + parseArgs(ZINTERSTORE, 'destination', 'source', { AGGREGATE: 'SUM' }), ['ZINTERSTORE', 'destination', '1', 'source', 'AGGREGATE', 'SUM'] diff --git a/packages/client/lib/commands/ZINTERSTORE.ts b/packages/client/lib/commands/ZINTERSTORE.ts index a5334566d73..dcbe153cfc7 100644 --- a/packages/client/lib/commands/ZINTERSTORE.ts +++ b/packages/client/lib/commands/ZINTERSTORE.ts @@ -1,17 +1,20 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { pushZInterArguments, ZInterOptions } from './ZINTER'; import { ZKeys } from './generic-transformers'; +import { parseZInterArguments, ZInterOptions } from './ZINTER'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, destination: RedisArgument, keys: ZKeys, options?: ZInterOptions ) { - return pushZInterArguments(['ZINTERSTORE', destination], keys, options); + parser.push('ZINTERSTORE'); + parser.pushKey(destination); + parseZInterArguments(parser, keys, options); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts b/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts index 05790510e49..234b250b143 100644 --- a/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZINTER_WITHSCORES.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZINTER_WITHSCORES from './ZINTER_WITHSCORES'; +import { parseArgs } from './generic-transformers'; describe('ZINTER WITHSCORES', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,21 +9,21 @@ describe('ZINTER WITHSCORES', () => { describe('transformArguments', () => { it('key (string)', () => { assert.deepEqual( - ZINTER_WITHSCORES.transformArguments('key'), + parseArgs(ZINTER_WITHSCORES, 'key'), ['ZINTER', '1', 'key', 'WITHSCORES'] ); }); it('keys (Array)', () => { assert.deepEqual( - ZINTER_WITHSCORES.transformArguments(['1', '2']), + parseArgs(ZINTER_WITHSCORES, ['1', '2']), ['ZINTER', '2', '1', '2', 'WITHSCORES'] ); }); it('key & weight', () => { assert.deepEqual( - ZINTER_WITHSCORES.transformArguments({ + parseArgs(ZINTER_WITHSCORES, { key: 'key', weight: 1 }), @@ -32,7 +33,7 @@ describe('ZINTER WITHSCORES', () => { it('keys & weights', () => { assert.deepEqual( - ZINTER_WITHSCORES.transformArguments([{ + parseArgs(ZINTER_WITHSCORES, [{ key: 'a', weight: 1 }, { @@ -45,7 +46,7 @@ describe('ZINTER WITHSCORES', () => { it('with AGGREGATE', () => { assert.deepEqual( - ZINTER_WITHSCORES.transformArguments('key', { + parseArgs(ZINTER_WITHSCORES, 'key', { AGGREGATE: 'SUM' }), ['ZINTER', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES'] diff --git a/packages/client/lib/commands/ZINTER_WITHSCORES.ts b/packages/client/lib/commands/ZINTER_WITHSCORES.ts index b287649fbc0..d3a6614b3c2 100644 --- a/packages/client/lib/commands/ZINTER_WITHSCORES.ts +++ b/packages/client/lib/commands/ZINTER_WITHSCORES.ts @@ -1,14 +1,13 @@ import { Command } from '../RESP/types'; -import ZINTER from './ZINTER'; import { transformSortedSetReply } from './generic-transformers'; +import ZINTER from './ZINTER'; + export default { - FIRST_KEY_INDEX: ZINTER.FIRST_KEY_INDEX, IS_READ_ONLY: ZINTER.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = ZINTER.transformArguments(...args); - redisArgs.push('WITHSCORES'); - return redisArgs; + parseCommand(...args: Parameters) { + ZINTER.parseCommand(...args); + args[0].push('WITHSCORES'); }, transformReply: transformSortedSetReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZLEXCOUNT.spec.ts b/packages/client/lib/commands/ZLEXCOUNT.spec.ts index d0a76075579..78c7411affd 100644 --- a/packages/client/lib/commands/ZLEXCOUNT.spec.ts +++ b/packages/client/lib/commands/ZLEXCOUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZLEXCOUNT from './ZLEXCOUNT'; +import { parseArgs } from './generic-transformers'; describe('ZLEXCOUNT', () => { it('transformArguments', () => { assert.deepEqual( - ZLEXCOUNT.transformArguments('key', '[a', '[b'), + parseArgs(ZLEXCOUNT, 'key', '[a', '[b'), ['ZLEXCOUNT', 'key', '[a', '[b'] ); }); diff --git a/packages/client/lib/commands/ZLEXCOUNT.ts b/packages/client/lib/commands/ZLEXCOUNT.ts index 26c9e0d70ac..7536590c168 100644 --- a/packages/client/lib/commands/ZLEXCOUNT.ts +++ b/packages/client/lib/commands/ZLEXCOUNT.ts @@ -1,19 +1,19 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, min: RedisArgument, max: RedisArgument ) { - return [ - 'ZLEXCOUNT', - key, - min, - max - ]; + parser.push('ZLEXCOUNT'); + parser.pushKey(key); + parser.push(min); + parser.push(max); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZMPOP.spec.ts b/packages/client/lib/commands/ZMPOP.spec.ts index 6335fca81cb..c15a53b7313 100644 --- a/packages/client/lib/commands/ZMPOP.spec.ts +++ b/packages/client/lib/commands/ZMPOP.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZMPOP from './ZMPOP'; +import { parseArgs } from './generic-transformers'; describe('ZMPOP', () => { testUtils.isVersionGreaterThanHook([7]); @@ -8,14 +9,14 @@ describe('ZMPOP', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ZMPOP.transformArguments('key', 'MIN'), + parseArgs(ZMPOP, 'key', 'MIN'), ['ZMPOP', '1', 'key', 'MIN'] ); }); it('with count', () => { assert.deepEqual( - ZMPOP.transformArguments('key', 'MIN', { + parseArgs(ZMPOP, 'key', 'MIN', { COUNT: 2 }), ['ZMPOP', '1', 'key', 'MIN', 'COUNT', '2'] diff --git a/packages/client/lib/commands/ZMPOP.ts b/packages/client/lib/commands/ZMPOP.ts index 57d2cccdaca..0e47108e25f 100644 --- a/packages/client/lib/commands/ZMPOP.ts +++ b/packages/client/lib/commands/ZMPOP.ts @@ -1,5 +1,6 @@ -import { RedisArgument, NullReply, TuplesReply, BlobStringReply, DoubleReply, ArrayReply, UnwrapReply, Resp2Reply, Command, TypeMapping } from '../RESP/types'; -import { pushVariadicArgument, RedisVariadicArgument, SortedSetSide, transformSortedSetReply, transformDoubleReply } from './generic-transformers'; +import { CommandParser } from '../client/parser'; +import { NullReply, TuplesReply, BlobStringReply, DoubleReply, ArrayReply, UnwrapReply, Resp2Reply, Command, TypeMapping } from '../RESP/types'; +import { RedisVariadicArgument, SortedSetSide, transformSortedSetReply, transformDoubleReply, Tail } from './generic-transformers'; export interface ZMPopOptions { COUNT?: number; @@ -13,30 +14,33 @@ export type ZMPopRawReply = NullReply | TuplesReply<[ ]>> ]>; -export function transformZMPopArguments( - args: Array, +export function parseZMPopArguments( + parser: CommandParser, keys: RedisVariadicArgument, side: SortedSetSide, options?: ZMPopOptions ) { - args = pushVariadicArgument(args, keys); + parser.pushKeysLength(keys); - args.push(side); + parser.push(side); if (options?.COUNT) { - args.push('COUNT', options.COUNT.toString()); + parser.push('COUNT', options.COUNT.toString()); } - - return args; } -export type ZMPopArguments = typeof transformZMPopArguments extends (_: any, ...args: infer T) => any ? T : never; +export type ZMPopArguments = Tail>; export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: false, - transformArguments(...args: ZMPopArguments) { - return transformZMPopArguments(['ZMPOP'], ...args); + parseCommand( + parser: CommandParser, + keys: RedisVariadicArgument, + side: SortedSetSide, + options?: ZMPopOptions + ) { + parser.push('ZMPOP'); + parseZMPopArguments(parser, keys, side, options) }, transformReply: { 2(reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) { diff --git a/packages/client/lib/commands/ZMSCORE.spec.ts b/packages/client/lib/commands/ZMSCORE.spec.ts index 5035724b117..6c6d2946e00 100644 --- a/packages/client/lib/commands/ZMSCORE.spec.ts +++ b/packages/client/lib/commands/ZMSCORE.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZMSCORE from './ZMSCORE'; +import { parseArgs } from './generic-transformers'; describe('ZMSCORE', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,14 +9,14 @@ describe('ZMSCORE', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - ZMSCORE.transformArguments('key', 'member'), + parseArgs(ZMSCORE, 'key', 'member'), ['ZMSCORE', 'key', 'member'] ); }); it('array', () => { assert.deepEqual( - ZMSCORE.transformArguments('key', ['1', '2']), + parseArgs(ZMSCORE, 'key', ['1', '2']), ['ZMSCORE', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/ZMSCORE.ts b/packages/client/lib/commands/ZMSCORE.ts index 00ade13b011..b225b35dfd3 100644 --- a/packages/client/lib/commands/ZMSCORE.ts +++ b/packages/client/lib/commands/ZMSCORE.ts @@ -1,14 +1,14 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, NullReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; -import { createTransformNullableDoubleReplyResp2Func, pushVariadicArguments, RedisVariadicArgument } from './generic-transformers'; +import { createTransformNullableDoubleReplyResp2Func, RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( - key: RedisArgument, - member: RedisVariadicArgument - ) { - return pushVariadicArguments(['ZMSCORE', key], member); + parseCommand(parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument) { + parser.push('ZMSCORE'); + parser.pushKey(key); + parser.pushVariadic(member); }, transformReply: { 2: (reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { diff --git a/packages/client/lib/commands/ZPOPMAX.spec.ts b/packages/client/lib/commands/ZPOPMAX.spec.ts index 609ccb826b5..1796647df86 100644 --- a/packages/client/lib/commands/ZPOPMAX.spec.ts +++ b/packages/client/lib/commands/ZPOPMAX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZPOPMAX from './ZPOPMAX'; +import { parseArgs } from './generic-transformers'; describe('ZPOPMAX', () => { it('transformArguments', () => { assert.deepEqual( - ZPOPMAX.transformArguments('key'), + parseArgs(ZPOPMAX, 'key'), ['ZPOPMAX', 'key'] ); }); diff --git a/packages/client/lib/commands/ZPOPMAX.ts b/packages/client/lib/commands/ZPOPMAX.ts index 130309347a6..05c7f35e052 100644 --- a/packages/client/lib/commands/ZPOPMAX.ts +++ b/packages/client/lib/commands/ZPOPMAX.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, TuplesReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '../RESP/types'; import { transformDoubleReply } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument) { - return ['ZPOPMAX', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('ZPOPMAX'); + parser.pushKey(key); }, transformReply: { 2: (reply: UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { diff --git a/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts b/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts index b653b1f3f1a..dd9d85dbd36 100644 --- a/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts +++ b/packages/client/lib/commands/ZPOPMAX_COUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZPOPMAX_COUNT from './ZPOPMAX_COUNT'; +import { parseArgs } from './generic-transformers'; describe('ZPOPMAX COUNT', () => { it('transformArguments', () => { assert.deepEqual( - ZPOPMAX_COUNT.transformArguments('key', 1), + parseArgs(ZPOPMAX_COUNT, 'key', 1), ['ZPOPMAX', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/ZPOPMAX_COUNT.ts b/packages/client/lib/commands/ZPOPMAX_COUNT.ts index 00d39536ae1..888ce039fbe 100644 --- a/packages/client/lib/commands/ZPOPMAX_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMAX_COUNT.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, Command } from '../RESP/types'; import { transformSortedSetReply } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, count: number) { - return ['ZPOPMAX', key, count.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + parser.push('ZPOPMAX'); + parser.pushKey(key); + parser.push(count.toString()); }, transformReply: transformSortedSetReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZPOPMIN.spec.ts b/packages/client/lib/commands/ZPOPMIN.spec.ts index 0b2c57d28b9..653a4e70a92 100644 --- a/packages/client/lib/commands/ZPOPMIN.spec.ts +++ b/packages/client/lib/commands/ZPOPMIN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZPOPMIN from './ZPOPMIN'; +import { parseArgs } from './generic-transformers'; describe('ZPOPMIN', () => { it('transformArguments', () => { assert.deepEqual( - ZPOPMIN.transformArguments('key'), + parseArgs(ZPOPMIN, 'key'), ['ZPOPMIN', 'key'] ); }); diff --git a/packages/client/lib/commands/ZPOPMIN.ts b/packages/client/lib/commands/ZPOPMIN.ts index b9da85cc974..6295925aef1 100644 --- a/packages/client/lib/commands/ZPOPMIN.ts +++ b/packages/client/lib/commands/ZPOPMIN.ts @@ -1,11 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, Command } from '../RESP/types'; import ZPOPMAX from './ZPOPMAX'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument) { - return ['ZPOPMIN', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('ZPOPMIN'); + parser.pushKey(key); }, transformReply: ZPOPMAX.transformReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts b/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts index fa3d9e2a975..126a3cc1e9a 100644 --- a/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts +++ b/packages/client/lib/commands/ZPOPMIN_COUNT.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZPOPMIN_COUNT from './ZPOPMIN_COUNT'; +import { parseArgs } from './generic-transformers'; describe('ZPOPMIN COUNT', () => { it('transformArguments', () => { assert.deepEqual( - ZPOPMIN_COUNT.transformArguments('key', 1), + parseArgs(ZPOPMIN_COUNT, 'key', 1), ['ZPOPMIN', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/ZPOPMIN_COUNT.ts b/packages/client/lib/commands/ZPOPMIN_COUNT.ts index 2433686da56..2b6abf580b9 100644 --- a/packages/client/lib/commands/ZPOPMIN_COUNT.ts +++ b/packages/client/lib/commands/ZPOPMIN_COUNT.ts @@ -1,11 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, Command } from '../RESP/types'; import { transformSortedSetReply } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, count: number) { - return ['ZPOPMIN', key, count.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + parser.push('ZPOPMIN'); + parser.pushKey(key); + parser.push(count.toString()); }, transformReply: transformSortedSetReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANDMEMBER.spec.ts b/packages/client/lib/commands/ZRANDMEMBER.spec.ts index 519850f5eff..a25ea79f8e1 100644 --- a/packages/client/lib/commands/ZRANDMEMBER.spec.ts +++ b/packages/client/lib/commands/ZRANDMEMBER.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANDMEMBER from './ZRANDMEMBER'; +import { parseArgs } from './generic-transformers'; describe('ZRANDMEMBER', () => { testUtils.isVersionGreaterThanHook([6, 2]); it('transformArguments', () => { assert.deepEqual( - ZRANDMEMBER.transformArguments('key'), + parseArgs(ZRANDMEMBER, 'key'), ['ZRANDMEMBER', 'key'] ); }); diff --git a/packages/client/lib/commands/ZRANDMEMBER.ts b/packages/client/lib/commands/ZRANDMEMBER.ts index 449eb281c66..2abd9d3684c 100644 --- a/packages/client/lib/commands/ZRANDMEMBER.ts +++ b/packages/client/lib/commands/ZRANDMEMBER.ts @@ -1,10 +1,11 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, BlobStringReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['ZRANDMEMBER', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('ZRANDMEMBER'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts index 2d0f4b9ced8..eee0d454975 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; +import { parseArgs } from './generic-transformers'; describe('ZRANDMEMBER COUNT', () => { testUtils.isVersionGreaterThanHook([6, 2, 5]); it('transformArguments', () => { assert.deepEqual( - ZRANDMEMBER_COUNT.transformArguments('key', 1), + parseArgs(ZRANDMEMBER_COUNT, 'key', 1), ['ZRANDMEMBER', 'key', '1'] ); }); diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts index 89b921f007a..42ef8110639 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT.ts @@ -1,13 +1,12 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; import ZRANDMEMBER from './ZRANDMEMBER'; export default { - FIRST_KEY_INDEX: ZRANDMEMBER.FIRST_KEY_INDEX, IS_READ_ONLY: ZRANDMEMBER.IS_READ_ONLY, - transformArguments(key: RedisArgument, count: number) { - const args = ZRANDMEMBER.transformArguments(key); - args.push(count.toString()); - return args; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + ZRANDMEMBER.parseCommand(parser, key); + parser.push(count.toString()); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts index aeeea3f6e71..3be3b92aeef 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANDMEMBER_COUNT_WITHSCORES from './ZRANDMEMBER_COUNT_WITHSCORES'; +import { parseArgs } from './generic-transformers'; describe('ZRANDMEMBER COUNT WITHSCORES', () => { testUtils.isVersionGreaterThanHook([6, 2, 5]); it('transformArguments', () => { assert.deepEqual( - ZRANDMEMBER_COUNT_WITHSCORES.transformArguments('key', 1), + parseArgs(ZRANDMEMBER_COUNT_WITHSCORES, 'key', 1), ['ZRANDMEMBER', 'key', '1', 'WITHSCORES'] ); }); diff --git a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts index 14c28d4b6c6..f096e9d807d 100644 --- a/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANDMEMBER_COUNT_WITHSCORES.ts @@ -1,14 +1,13 @@ +import { CommandParser } from '../client/parser'; import { Command, RedisArgument } from '../RESP/types'; -import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; import { transformSortedSetReply } from './generic-transformers'; +import ZRANDMEMBER_COUNT from './ZRANDMEMBER_COUNT'; export default { - FIRST_KEY_INDEX: ZRANDMEMBER_COUNT.FIRST_KEY_INDEX, IS_READ_ONLY: ZRANDMEMBER_COUNT.IS_READ_ONLY, - transformArguments(key: RedisArgument, count: number) { - const args = ZRANDMEMBER_COUNT.transformArguments(key, count); - args.push('WITHSCORES'); - return args; + parseCommand(parser: CommandParser, key: RedisArgument, count: number) { + ZRANDMEMBER_COUNT.parseCommand(parser, key, count); + parser.push('WITHSCORES'); }, transformReply: transformSortedSetReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGE.spec.ts b/packages/client/lib/commands/ZRANGE.spec.ts index db940062b2f..a780e4ef613 100644 --- a/packages/client/lib/commands/ZRANGE.spec.ts +++ b/packages/client/lib/commands/ZRANGE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANGE from './ZRANGE'; +import { parseArgs } from './generic-transformers'; describe('ZRANGE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ZRANGE.transformArguments('src', 0, 1), + parseArgs(ZRANGE, 'src', 0, 1), ['ZRANGE', 'src', '0', '1'] ); }); it('with BYSCORE', () => { assert.deepEqual( - ZRANGE.transformArguments('src', 0, 1, { + parseArgs(ZRANGE, 'src', 0, 1, { BY: 'SCORE' }), ['ZRANGE', 'src', '0', '1', 'BYSCORE'] @@ -22,7 +23,7 @@ describe('ZRANGE', () => { it('with BYLEX', () => { assert.deepEqual( - ZRANGE.transformArguments('src', 0, 1, { + parseArgs(ZRANGE, 'src', 0, 1, { BY: 'LEX' }), ['ZRANGE', 'src', '0', '1', 'BYLEX'] @@ -31,7 +32,7 @@ describe('ZRANGE', () => { it('with REV', () => { assert.deepEqual( - ZRANGE.transformArguments('src', 0, 1, { + parseArgs(ZRANGE, 'src', 0, 1, { REV: true }), ['ZRANGE', 'src', '0', '1', 'REV'] @@ -40,7 +41,7 @@ describe('ZRANGE', () => { it('with LIMIT', () => { assert.deepEqual( - ZRANGE.transformArguments('src', 0, 1, { + parseArgs(ZRANGE, 'src', 0, 1, { LIMIT: { offset: 0, count: 1 @@ -52,7 +53,7 @@ describe('ZRANGE', () => { it('with BY & REV & LIMIT', () => { assert.deepEqual( - ZRANGE.transformArguments('src', 0, 1, { + parseArgs(ZRANGE, 'src', 0, 1, { BY: 'SCORE', REV: true, LIMIT: { diff --git a/packages/client/lib/commands/ZRANGE.ts b/packages/client/lib/commands/ZRANGE.ts index 557044b67da..d1bc3433a50 100644 --- a/packages/client/lib/commands/ZRANGE.ts +++ b/packages/client/lib/commands/ZRANGE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; @@ -10,45 +11,54 @@ export interface ZRangeOptions { }; } +export function zRangeArgument( + min: RedisArgument | number, + max: RedisArgument | number, + options?: ZRangeOptions +) { + const args = [ + transformStringDoubleArgument(min), + transformStringDoubleArgument(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 default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, min: RedisArgument | number, max: RedisArgument | number, options?: ZRangeOptions ) { - const args = [ - 'ZRANGE', - key, - transformStringDoubleArgument(min), - transformStringDoubleArgument(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; + parser.push('ZRANGE'); + parser.pushKey(key); + parser.pushVariadic(zRangeArgument(min, max, options)) }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGEBYLEX.spec.ts b/packages/client/lib/commands/ZRANGEBYLEX.spec.ts index f3f6f4bc0e5..942e184661a 100644 --- a/packages/client/lib/commands/ZRANGEBYLEX.spec.ts +++ b/packages/client/lib/commands/ZRANGEBYLEX.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANGEBYLEX from './ZRANGEBYLEX'; +import { parseArgs } from './generic-transformers'; describe('ZRANGEBYLEX', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ZRANGEBYLEX.transformArguments('src', '-', '+'), + parseArgs(ZRANGEBYLEX, 'src', '-', '+'), ['ZRANGEBYLEX', 'src', '-', '+'] ); }); it('with LIMIT', () => { assert.deepEqual( - ZRANGEBYLEX.transformArguments('src', '-', '+', { + parseArgs(ZRANGEBYLEX, 'src', '-', '+', { LIMIT: { offset: 0, count: 1 diff --git a/packages/client/lib/commands/ZRANGEBYLEX.ts b/packages/client/lib/commands/ZRANGEBYLEX.ts index afe7718f3c3..316d9745c7e 100644 --- a/packages/client/lib/commands/ZRANGEBYLEX.ts +++ b/packages/client/lib/commands/ZRANGEBYLEX.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; @@ -9,26 +10,25 @@ export interface ZRangeByLexOptions { } export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, min: RedisArgument, max: RedisArgument, options?: ZRangeByLexOptions ) { - const args = [ - 'ZRANGEBYLEX', - key, + parser.push('ZRANGEBYLEX'); + parser.pushKey(key); + parser.push( transformStringDoubleArgument(min), transformStringDoubleArgument(max) - ]; + ); if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); + parser.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts b/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts index 61267ea7f2f..364882f21a9 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANGEBYSCORE from './ZRANGEBYSCORE'; +import { parseArgs } from './generic-transformers'; describe('ZRANGEBYSCORE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ZRANGEBYSCORE.transformArguments('src', 0, 1), + parseArgs(ZRANGEBYSCORE, 'src', 0, 1), ['ZRANGEBYSCORE', 'src', '0', '1'] ); }); it('with LIMIT', () => { assert.deepEqual( - ZRANGEBYSCORE.transformArguments('src', 0, 1, { + parseArgs(ZRANGEBYSCORE, 'src', 0, 1, { LIMIT: { offset: 0, count: 1 diff --git a/packages/client/lib/commands/ZRANGEBYSCORE.ts b/packages/client/lib/commands/ZRANGEBYSCORE.ts index e54c96380de..4d5471fdc0b 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; @@ -11,26 +12,25 @@ export interface ZRangeByScoreOptions { export declare function transformReply(): Array; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, min: string | number, max: string | number, options?: ZRangeByScoreOptions ) { - const args = [ - 'ZRANGEBYSCORE', - key, + parser.push('ZRANGEBYSCORE'); + parser.pushKey(key); + parser.push( transformStringDoubleArgument(min), transformStringDoubleArgument(max) - ]; + ); if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); + parser.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts index e70a97b0372..191eaa4e34f 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANGEBYSCORE_WITHSCORES from './ZRANGEBYSCORE_WITHSCORES'; +import { parseArgs } from './generic-transformers'; describe('ZRANGEBYSCORE WITHSCORES', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ZRANGEBYSCORE_WITHSCORES.transformArguments('src', 0, 1), + parseArgs(ZRANGEBYSCORE_WITHSCORES, 'src', 0, 1), ['ZRANGEBYSCORE', 'src', '0', '1', 'WITHSCORES'] ); }); it('with LIMIT', () => { assert.deepEqual( - ZRANGEBYSCORE_WITHSCORES.transformArguments('src', 0, 1, { + parseArgs(ZRANGEBYSCORE_WITHSCORES, 'src', 0, 1, { LIMIT: { offset: 0, count: 1 diff --git a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts index bfbe09c6e26..1a759b23dce 100644 --- a/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGEBYSCORE_WITHSCORES.ts @@ -1,14 +1,15 @@ import { Command } from '../RESP/types'; -import ZRANGEBYSCORE from './ZRANGEBYSCORE'; import { transformSortedSetReply } from './generic-transformers'; +import ZRANGEBYSCORE from './ZRANGEBYSCORE'; export default { - FIRST_KEY_INDEX: ZRANGEBYSCORE.FIRST_KEY_INDEX, + CACHEABLE: ZRANGEBYSCORE.CACHEABLE, IS_READ_ONLY: ZRANGEBYSCORE.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = ZRANGEBYSCORE.transformArguments(...args); - redisArgs.push('WITHSCORES'); - return redisArgs; + parseCommand(...args: Parameters) { + const parser = args[0]; + + ZRANGEBYSCORE.parseCommand(...args); + parser.push('WITHSCORES'); }, transformReply: transformSortedSetReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGESTORE.spec.ts b/packages/client/lib/commands/ZRANGESTORE.spec.ts index 51315d3463b..c9708efd6fd 100644 --- a/packages/client/lib/commands/ZRANGESTORE.spec.ts +++ b/packages/client/lib/commands/ZRANGESTORE.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANGESTORE from './ZRANGESTORE'; +import { parseArgs } from './generic-transformers'; describe('ZRANGESTORE', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,14 +9,14 @@ describe('ZRANGESTORE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ZRANGESTORE.transformArguments('destination', 'source', 0, 1), + parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1), ['ZRANGESTORE', 'destination', 'source', '0', '1'] ); }); it('with BYSCORE', () => { assert.deepEqual( - ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, { BY: 'SCORE' }), ['ZRANGESTORE', 'destination', 'source', '0', '1', 'BYSCORE'] @@ -24,7 +25,7 @@ describe('ZRANGESTORE', () => { it('with BYLEX', () => { assert.deepEqual( - ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, { BY: 'LEX' }), ['ZRANGESTORE', 'destination', 'source', '0', '1', 'BYLEX'] @@ -33,7 +34,7 @@ describe('ZRANGESTORE', () => { it('with REV', () => { assert.deepEqual( - ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, { REV: true }), ['ZRANGESTORE', 'destination', 'source', '0', '1', 'REV'] @@ -42,7 +43,7 @@ describe('ZRANGESTORE', () => { it('with LIMIT', () => { assert.deepEqual( - ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, { LIMIT: { offset: 0, count: 1 @@ -54,7 +55,7 @@ describe('ZRANGESTORE', () => { it('with BY & REV & LIMIT', () => { assert.deepEqual( - ZRANGESTORE.transformArguments('destination', 'source', 0, 1, { + parseArgs(ZRANGESTORE, 'destination', 'source', 0, 1, { BY: 'SCORE', REV: true, LIMIT: { diff --git a/packages/client/lib/commands/ZRANGESTORE.ts b/packages/client/lib/commands/ZRANGESTORE.ts index 96f10120b87..f73e93a506f 100644 --- a/packages/client/lib/commands/ZRANGESTORE.ts +++ b/packages/client/lib/commands/ZRANGESTORE.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; @@ -11,42 +12,40 @@ export interface ZRangeStoreOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, destination: RedisArgument, source: RedisArgument, min: RedisArgument | number, max: RedisArgument | number, options?: ZRangeStoreOptions ) { - const args = [ - 'ZRANGESTORE', - destination, - source, - transformStringDoubleArgument(min), + parser.push('ZRANGESTORE'); + parser.pushKey(destination); + parser.pushKey(source); + parser.push( + transformStringDoubleArgument(min), transformStringDoubleArgument(max) - ]; + ); switch (options?.BY) { case 'SCORE': - args.push('BYSCORE'); + parser.push('BYSCORE'); break; case 'LEX': - args.push('BYLEX'); + parser.push('BYLEX'); break; } if (options?.REV) { - args.push('REV'); + parser.push('REV'); } if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); + parser.push('LIMIT', options.LIMIT.offset.toString(), options.LIMIT.count.toString()); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts b/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts index 038c150a675..e3009a6eadb 100644 --- a/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANGE_WITHSCORES from './ZRANGE_WITHSCORES'; +import { parseArgs } from './generic-transformers'; describe('ZRANGE WITHSCORES', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ZRANGE_WITHSCORES.transformArguments('src', 0, 1), + parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1), ['ZRANGE', 'src', '0', '1', 'WITHSCORES'] ); }); it('with BY', () => { assert.deepEqual( - ZRANGE_WITHSCORES.transformArguments('src', 0, 1, { + parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1, { BY: 'SCORE' }), ['ZRANGE', 'src', '0', '1', 'BYSCORE', 'WITHSCORES'] @@ -22,7 +23,7 @@ describe('ZRANGE WITHSCORES', () => { it('with REV', () => { assert.deepEqual( - ZRANGE_WITHSCORES.transformArguments('src', 0, 1, { + parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1, { REV: true }), ['ZRANGE', 'src', '0', '1', 'REV', 'WITHSCORES'] @@ -31,7 +32,7 @@ describe('ZRANGE WITHSCORES', () => { it('with LIMIT', () => { assert.deepEqual( - ZRANGE_WITHSCORES.transformArguments('src', 0, 1, { + parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1, { LIMIT: { offset: 0, count: 1 @@ -43,7 +44,7 @@ describe('ZRANGE WITHSCORES', () => { it('with BY & REV & LIMIT', () => { assert.deepEqual( - ZRANGE_WITHSCORES.transformArguments('src', 0, 1, { + parseArgs(ZRANGE_WITHSCORES, 'src', 0, 1, { BY: 'SCORE', REV: true, LIMIT: { diff --git a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts index cfa90e99ea4..7e6cf00cf2e 100644 --- a/packages/client/lib/commands/ZRANGE_WITHSCORES.ts +++ b/packages/client/lib/commands/ZRANGE_WITHSCORES.ts @@ -1,14 +1,15 @@ import { Command } from '../RESP/types'; -import ZRANGE from './ZRANGE'; import { transformSortedSetReply } from './generic-transformers'; +import ZRANGE from './ZRANGE'; export default { - FIRST_KEY_INDEX: ZRANGE.FIRST_KEY_INDEX, + CACHEABLE: ZRANGE.CACHEABLE, IS_READ_ONLY: ZRANGE.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = ZRANGE.transformArguments(...args); - redisArgs.push('WITHSCORES'); - return redisArgs; + parseCommand(...args: Parameters) { + const parser = args[0]; + + ZRANGE.parseCommand(...args); + parser.push('WITHSCORES'); }, transformReply: transformSortedSetReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANK.spec.ts b/packages/client/lib/commands/ZRANK.spec.ts index 9341709bda3..480f75f66e1 100644 --- a/packages/client/lib/commands/ZRANK.spec.ts +++ b/packages/client/lib/commands/ZRANK.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANK from './ZRANK'; +import { parseArgs } from './generic-transformers'; describe('ZRANK', () => { it('transformArguments', () => { assert.deepEqual( - ZRANK.transformArguments('key', 'member'), + parseArgs(ZRANK, 'key', 'member'), ['ZRANK', 'key', 'member'] ); }); diff --git a/packages/client/lib/commands/ZRANK.ts b/packages/client/lib/commands/ZRANK.ts index 11184c0a28f..045e9ef8c25 100644 --- a/packages/client/lib/commands/ZRANK.ts +++ b/packages/client/lib/commands/ZRANK.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, NullReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, member: RedisArgument) { - return ['ZRANK', key, member]; + parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { + parser.push('ZRANK'); + parser.pushKey(key); + parser.push(member); }, transformReply: undefined as unknown as () => NumberReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts index b571e0f7071..9fa7cb1f6fd 100644 --- a/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.spec.ts @@ -1,13 +1,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZRANK_WITHSCORE from './ZRANK_WITHSCORE'; +import { parseArgs } from './generic-transformers'; describe('ZRANK WITHSCORE', () => { testUtils.isVersionGreaterThanHook([7, 2]); it('transformArguments', () => { assert.deepEqual( - ZRANK_WITHSCORE.transformArguments('key', 'member'), + parseArgs(ZRANK_WITHSCORE, 'key', 'member'), ['ZRANK', 'key', 'member', 'WITHSCORE'] ); }); diff --git a/packages/client/lib/commands/ZRANK_WITHSCORE.ts b/packages/client/lib/commands/ZRANK_WITHSCORE.ts index 39c788535e3..dc2e48b362d 100644 --- a/packages/client/lib/commands/ZRANK_WITHSCORE.ts +++ b/packages/client/lib/commands/ZRANK_WITHSCORE.ts @@ -2,12 +2,13 @@ import { NullReply, TuplesReply, NumberReply, BlobStringReply, DoubleReply, Unwr import ZRANK from './ZRANK'; export default { - FIRST_KEY_INDEX: ZRANK.FIRST_KEY_INDEX, + CACHEABLE: ZRANK.CACHEABLE, IS_READ_ONLY: ZRANK.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = ZRANK.transformArguments(...args); - redisArgs.push('WITHSCORE'); - return redisArgs; + parseCommand(...args: Parameters) { + const parser = args[0]; + + ZRANK.parseCommand(...args); + parser.push('WITHSCORE'); }, transformReply: { 2: (reply: UnwrapReply>) => { diff --git a/packages/client/lib/commands/ZREM.spec.ts b/packages/client/lib/commands/ZREM.spec.ts index 4b203c9f4eb..ac65b3d0139 100644 --- a/packages/client/lib/commands/ZREM.spec.ts +++ b/packages/client/lib/commands/ZREM.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZREM from './ZREM'; +import { parseArgs } from './generic-transformers'; describe('ZREM', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - ZREM.transformArguments('key', 'member'), + parseArgs(ZREM, 'key', 'member'), ['ZREM', 'key', 'member'] ); }); it('array', () => { assert.deepEqual( - ZREM.transformArguments('key', ['1', '2']), + parseArgs(ZREM, 'key', ['1', '2']), ['ZREM', 'key', '1', '2'] ); }); diff --git a/packages/client/lib/commands/ZREM.ts b/packages/client/lib/commands/ZREM.ts index 54f55841fce..c8ba0ec02a6 100644 --- a/packages/client/lib/commands/ZREM.ts +++ b/packages/client/lib/commands/ZREM.ts @@ -1,14 +1,17 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from './generic-transformers'; +import { RedisVariadicArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, member: RedisVariadicArgument ) { - return pushVariadicArguments(['ZREM', key], member); + parser.push('ZREM'); + parser.pushKey(key); + parser.pushVariadic(member); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts b/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts index 9f29c3cdcf7..b141b7679ee 100644 --- a/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts +++ b/packages/client/lib/commands/ZREMRANGEBYLEX.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZREMRANGEBYLEX from './ZREMRANGEBYLEX'; +import { parseArgs } from './generic-transformers'; describe('ZREMRANGEBYLEX', () => { it('transformArguments', () => { assert.deepEqual( - ZREMRANGEBYLEX.transformArguments('key', '[a', '[b'), + parseArgs(ZREMRANGEBYLEX, 'key', '[a', '[b'), ['ZREMRANGEBYLEX', 'key', '[a', '[b'] ); }); diff --git a/packages/client/lib/commands/ZREMRANGEBYLEX.ts b/packages/client/lib/commands/ZREMRANGEBYLEX.ts index e3cd7013ac2..5d7e1a21bb0 100644 --- a/packages/client/lib/commands/ZREMRANGEBYLEX.ts +++ b/packages/client/lib/commands/ZREMRANGEBYLEX.ts @@ -1,20 +1,21 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, Command, RedisArgument } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, min: RedisArgument | number, max: RedisArgument | number ) { - return [ - 'ZREMRANGEBYLEX', - key, + parser.push('ZREMRANGEBYLEX'); + parser.pushKey(key); + parser.push( transformStringDoubleArgument(min), transformStringDoubleArgument(max) - ]; + ); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts b/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts index 12627083e1a..19f54466c20 100644 --- a/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts +++ b/packages/client/lib/commands/ZREMRANGEBYRANK.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZREMRANGEBYRANK from './ZREMRANGEBYRANK'; +import { parseArgs } from './generic-transformers'; describe('ZREMRANGEBYRANK', () => { it('transformArguments', () => { assert.deepEqual( - ZREMRANGEBYRANK.transformArguments('key', 0, 1), + parseArgs(ZREMRANGEBYRANK, 'key', 0, 1), ['ZREMRANGEBYRANK', 'key', '0', '1'] ); }); diff --git a/packages/client/lib/commands/ZREMRANGEBYRANK.ts b/packages/client/lib/commands/ZREMRANGEBYRANK.ts index 986de33060e..0a2eb3fadf3 100644 --- a/packages/client/lib/commands/ZREMRANGEBYRANK.ts +++ b/packages/client/lib/commands/ZREMRANGEBYRANK.ts @@ -1,13 +1,20 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, start: number, - stop: number) { - return ['ZREMRANGEBYRANK', key, start.toString(), stop.toString()]; + stop: number + ) { + parser.push('ZREMRANGEBYRANK'); + parser.pushKey(key); + parser.push( + start.toString(), + stop.toString() + ); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts b/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts index fb3ba4e718c..856692ef8f5 100644 --- a/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts +++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; import ZREMRANGEBYSCORE from './ZREMRANGEBYSCORE'; describe('ZREMRANGEBYSCORE', () => { it('transformArguments', () => { assert.deepEqual( - ZREMRANGEBYSCORE.transformArguments('key', 0, 1), + parseArgs(ZREMRANGEBYSCORE, 'key', 0, 1), ['ZREMRANGEBYSCORE', 'key', '0', '1'] ); }); diff --git a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts index 7050f2627a7..3d23d875948 100644 --- a/packages/client/lib/commands/ZREMRANGEBYSCORE.ts +++ b/packages/client/lib/commands/ZREMRANGEBYSCORE.ts @@ -1,20 +1,21 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command } from '../RESP/types'; import { transformStringDoubleArgument } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, min: RedisArgument | number, max: RedisArgument | number, ) { - return [ - 'ZREMRANGEBYSCORE', - key, + parser.push('ZREMRANGEBYSCORE'); + parser.pushKey(key); + parser.push( transformStringDoubleArgument(min), transformStringDoubleArgument(max) - ]; + ); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZREVRANK.spec.ts b/packages/client/lib/commands/ZREVRANK.spec.ts index 418773b6003..c89f528eb1c 100644 --- a/packages/client/lib/commands/ZREVRANK.spec.ts +++ b/packages/client/lib/commands/ZREVRANK.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; import ZREVRANK from './ZREVRANK'; describe('ZREVRANK', () => { it('transformArguments', () => { assert.deepEqual( - ZREVRANK.transformArguments('key', 'member'), + parseArgs(ZREVRANK, 'key', 'member'), ['ZREVRANK', 'key', 'member'] ); }); diff --git a/packages/client/lib/commands/ZREVRANK.ts b/packages/client/lib/commands/ZREVRANK.ts index 3bf52d21de5..d48dc68adc2 100644 --- a/packages/client/lib/commands/ZREVRANK.ts +++ b/packages/client/lib/commands/ZREVRANK.ts @@ -1,10 +1,13 @@ +import { CommandParser } from '../client/parser'; import { NumberReply, NullReply, Command, RedisArgument } from '../RESP/types'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, member: RedisArgument) { - return ['ZREVRANK', key, member]; + parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { + parser.push('ZREVRANK'); + parser.pushKey(key); + parser.push(member); }, transformReply: undefined as unknown as () => NumberReply | NullReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZSCAN.spec.ts b/packages/client/lib/commands/ZSCAN.spec.ts index ebeaad2a4d0..f8064aea41e 100644 --- a/packages/client/lib/commands/ZSCAN.spec.ts +++ b/packages/client/lib/commands/ZSCAN.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; +import { parseArgs } from './generic-transformers'; import ZSCAN from './ZSCAN'; describe('ZSCAN', () => { describe('transformArguments', () => { it('cusror only', () => { assert.deepEqual( - ZSCAN.transformArguments('key', '0'), + parseArgs(ZSCAN, 'key', '0'), ['ZSCAN', 'key', '0'] ); }); it('with MATCH', () => { assert.deepEqual( - ZSCAN.transformArguments('key', '0', { + parseArgs(ZSCAN, 'key', '0', { MATCH: 'pattern' }), ['ZSCAN', 'key', '0', 'MATCH', 'pattern'] @@ -22,7 +23,7 @@ describe('ZSCAN', () => { it('with COUNT', () => { assert.deepEqual( - ZSCAN.transformArguments('key', '0', { + parseArgs(ZSCAN, 'key', '0', { COUNT: 1 }), ['ZSCAN', 'key', '0', 'COUNT', '1'] @@ -31,7 +32,7 @@ describe('ZSCAN', () => { it('with MATCH & COUNT', () => { assert.deepEqual( - ZSCAN.transformArguments('key', '0', { + parseArgs(ZSCAN, 'key', '0', { MATCH: 'pattern', COUNT: 1 }), diff --git a/packages/client/lib/commands/ZSCAN.ts b/packages/client/lib/commands/ZSCAN.ts index 853cdf098f6..051235033eb 100644 --- a/packages/client/lib/commands/ZSCAN.ts +++ b/packages/client/lib/commands/ZSCAN.ts @@ -1,5 +1,6 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { ScanCommonOptions, pushScanArguments } from './SCAN'; +import { ScanCommonOptions, parseScanArguments } from './SCAN'; import { transformSortedSetReply } from './generic-transformers'; export interface HScanEntry { @@ -8,14 +9,16 @@ export interface HScanEntry { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, cursor: RedisArgument, options?: ScanCommonOptions ) { - return pushScanArguments(['ZSCAN', key], cursor, options); + parser.push('ZSCAN'); + parser.pushKey(key); + parseScanArguments(parser, cursor, options); }, transformReply([cursor, rawMembers]: [BlobStringReply, ArrayReply]) { return { diff --git a/packages/client/lib/commands/ZSCORE.spec.ts b/packages/client/lib/commands/ZSCORE.spec.ts index 3d8df6640c8..4229ab7aac0 100644 --- a/packages/client/lib/commands/ZSCORE.spec.ts +++ b/packages/client/lib/commands/ZSCORE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZSCORE from './ZSCORE'; +import { parseArgs } from './generic-transformers'; describe('ZSCORE', () => { it('transformArguments', () => { assert.deepEqual( - ZSCORE.transformArguments('key', 'member'), + parseArgs(ZSCORE, 'key', 'member'), ['ZSCORE', 'key', 'member'] ); }); diff --git a/packages/client/lib/commands/ZSCORE.ts b/packages/client/lib/commands/ZSCORE.ts index 0d3db752e1c..23b52901078 100644 --- a/packages/client/lib/commands/ZSCORE.ts +++ b/packages/client/lib/commands/ZSCORE.ts @@ -1,12 +1,15 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, Command } from '../RESP/types'; import { transformNullableDoubleReply } from './generic-transformers'; export default { - FIRST_KEY_INDEX: 1, + CACHEABLE: true, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, member: RedisArgument) { - return ['ZSCORE', key, member]; + parseCommand(parser: CommandParser, key: RedisArgument, member: RedisArgument) { + parser.push('ZSCORE'); + parser.pushKey(key); + parser.push(member); }, transformReply: transformNullableDoubleReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZUNION.spec.ts b/packages/client/lib/commands/ZUNION.spec.ts index f66bb7abc63..b4dbb4de603 100644 --- a/packages/client/lib/commands/ZUNION.spec.ts +++ b/packages/client/lib/commands/ZUNION.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZUNION from './ZUNION'; +import { parseArgs } from './generic-transformers'; describe('ZUNION', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,21 +9,21 @@ describe('ZUNION', () => { describe('transformArguments', () => { it('key (string)', () => { assert.deepEqual( - ZUNION.transformArguments('key'), + parseArgs(ZUNION, 'key'), ['ZUNION', '1', 'key'] ); }); it('keys (Array)', () => { assert.deepEqual( - ZUNION.transformArguments(['1', '2']), + parseArgs(ZUNION, ['1', '2']), ['ZUNION', '2', '1', '2'] ); }); it('key & weight', () => { assert.deepEqual( - ZUNION.transformArguments({ + parseArgs(ZUNION, { key: 'key', weight: 1 }), @@ -32,7 +33,7 @@ describe('ZUNION', () => { it('keys & weights', () => { assert.deepEqual( - ZUNION.transformArguments([{ + parseArgs(ZUNION, [{ key: 'a', weight: 1 }, { @@ -45,7 +46,7 @@ describe('ZUNION', () => { it('with AGGREGATE', () => { assert.deepEqual( - ZUNION.transformArguments('key', { + parseArgs(ZUNION, 'key', { AGGREGATE: 'SUM' }), ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM'] diff --git a/packages/client/lib/commands/ZUNION.ts b/packages/client/lib/commands/ZUNION.ts index 09614b9dc01..a91dc68bc09 100644 --- a/packages/client/lib/commands/ZUNION.ts +++ b/packages/client/lib/commands/ZUNION.ts @@ -1,24 +1,20 @@ +import { CommandParser } from '../client/parser'; import { ArrayReply, BlobStringReply, Command } from '../RESP/types'; -import { ZKeys, pushZKeysArguments } from './generic-transformers'; +import { ZKeys, parseZKeysArguments } from './generic-transformers'; export interface ZUnionOptions { AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; } export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: true, - transformArguments( - keys: ZKeys, - options?: ZUnionOptions - ) { - const args = pushZKeysArguments(['ZUNION'], keys); + parseCommand(parser: CommandParser, keys: ZKeys, options?: ZUnionOptions) { + parser.push('ZUNION'); + parseZKeysArguments(parser, keys); if (options?.AGGREGATE) { - args.push('AGGREGATE', options.AGGREGATE); + parser.push('AGGREGATE', options.AGGREGATE); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZUNIONSTORE.spec.ts b/packages/client/lib/commands/ZUNIONSTORE.spec.ts index 7a01e80f0c0..a369a649311 100644 --- a/packages/client/lib/commands/ZUNIONSTORE.spec.ts +++ b/packages/client/lib/commands/ZUNIONSTORE.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZUNIONSTORE from './ZUNIONSTORE'; +import { parseArgs } from './generic-transformers'; describe('ZUNIONSTORE', () => { describe('transformArguments', () => { it('key (string)', () => { assert.deepEqual( - ZUNIONSTORE.transformArguments('destination', 'source'), + parseArgs(ZUNIONSTORE, 'destination', 'source'), ['ZUNIONSTORE', 'destination', '1', 'source'] ); }); it('keys (Array)', () => { assert.deepEqual( - ZUNIONSTORE.transformArguments('destination', ['1', '2']), + parseArgs(ZUNIONSTORE, 'destination', ['1', '2']), ['ZUNIONSTORE', 'destination', '2', '1', '2'] ); }); it('key & weight', () => { assert.deepEqual( - ZUNIONSTORE.transformArguments('destination', { + parseArgs(ZUNIONSTORE, 'destination', { key: 'source', weight: 1 }), @@ -30,7 +31,7 @@ describe('ZUNIONSTORE', () => { it('keys & weights', () => { assert.deepEqual( - ZUNIONSTORE.transformArguments('destination', [{ + parseArgs(ZUNIONSTORE, 'destination', [{ key: 'a', weight: 1 }, { @@ -43,7 +44,7 @@ describe('ZUNIONSTORE', () => { it('with AGGREGATE', () => { assert.deepEqual( - ZUNIONSTORE.transformArguments('destination', 'source', { + parseArgs(ZUNIONSTORE, 'destination', 'source', { AGGREGATE: 'SUM' }), ['ZUNIONSTORE', 'destination', '1', 'source', 'AGGREGATE', 'SUM'] diff --git a/packages/client/lib/commands/ZUNIONSTORE.ts b/packages/client/lib/commands/ZUNIONSTORE.ts index a14d3ba31c9..c88f5a5a6f9 100644 --- a/packages/client/lib/commands/ZUNIONSTORE.ts +++ b/packages/client/lib/commands/ZUNIONSTORE.ts @@ -1,25 +1,26 @@ +import { CommandParser } from '../client/parser'; import { RedisArgument, NumberReply, Command, } from '../RESP/types'; -import { ZKeys, pushZKeysArguments } from './generic-transformers'; +import { ZKeys, parseZKeysArguments } from './generic-transformers'; export interface ZUnionOptions { AGGREGATE?: 'SUM' | 'MIN' | 'MAX'; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, destination: RedisArgument, keys: ZKeys, options?: ZUnionOptions - ) { - const args = pushZKeysArguments(['ZUNIONSTORE', destination], keys); - + ): any { + parser.push('ZUNIONSTORE'); + parser.pushKey(destination); + parseZKeysArguments(parser, keys); + if (options?.AGGREGATE) { - args.push('AGGREGATE', options.AGGREGATE); + parser.push('AGGREGATE', options.AGGREGATE); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts b/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts index bbf3ec676e8..dee735fc99f 100644 --- a/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts +++ b/packages/client/lib/commands/ZUNION_WITHSCORES.spec.ts @@ -1,6 +1,7 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ZUNION_WITHSCORES from './ZUNION_WITHSCORES'; +import { parseArgs } from './generic-transformers'; describe('ZUNION WITHSCORES', () => { testUtils.isVersionGreaterThanHook([6, 2]); @@ -8,21 +9,21 @@ describe('ZUNION WITHSCORES', () => { describe('transformArguments', () => { it('key (string)', () => { assert.deepEqual( - ZUNION_WITHSCORES.transformArguments('key'), + parseArgs(ZUNION_WITHSCORES, 'key'), ['ZUNION', '1', 'key', 'WITHSCORES'] ); }); it('keys (Array)', () => { assert.deepEqual( - ZUNION_WITHSCORES.transformArguments(['1', '2']), + parseArgs(ZUNION_WITHSCORES, ['1', '2']), ['ZUNION', '2', '1', '2', 'WITHSCORES'] ); }); it('key & weight', () => { assert.deepEqual( - ZUNION_WITHSCORES.transformArguments({ + parseArgs(ZUNION_WITHSCORES, { key: 'key', weight: 1 }), @@ -32,7 +33,7 @@ describe('ZUNION WITHSCORES', () => { it('keys & weights', () => { assert.deepEqual( - ZUNION_WITHSCORES.transformArguments([{ + parseArgs(ZUNION_WITHSCORES, [{ key: 'a', weight: 1 }, { @@ -45,7 +46,7 @@ describe('ZUNION WITHSCORES', () => { it('with AGGREGATE', () => { assert.deepEqual( - ZUNION_WITHSCORES.transformArguments('key', { + parseArgs(ZUNION_WITHSCORES, 'key', { AGGREGATE: 'SUM' }), ['ZUNION', '1', 'key', 'AGGREGATE', 'SUM', 'WITHSCORES'] diff --git a/packages/client/lib/commands/ZUNION_WITHSCORES.ts b/packages/client/lib/commands/ZUNION_WITHSCORES.ts index d0895a3de76..c62df55518f 100644 --- a/packages/client/lib/commands/ZUNION_WITHSCORES.ts +++ b/packages/client/lib/commands/ZUNION_WITHSCORES.ts @@ -1,14 +1,15 @@ import { Command } from '../RESP/types'; -import ZUNION from './ZUNION'; import { transformSortedSetReply } from './generic-transformers'; +import ZUNION from './ZUNION'; + export default { - FIRST_KEY_INDEX: ZUNION.FIRST_KEY_INDEX, IS_READ_ONLY: ZUNION.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = ZUNION.transformArguments(...args); - redisArgs.push('WITHSCORES'); - return redisArgs; + parseCommand(...args: Parameters) { + const parser = args[0]; + + ZUNION.parseCommand(...args); + parser.push('WITHSCORES'); }, transformReply: transformSortedSetReply } as const satisfies Command; diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index cc7100d90e6..fc139a948e0 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -1,5 +1,6 @@ +import { BasicCommandParser, CommandParser } from '../client/parser'; import { RESP_TYPES } from '../RESP/decoder'; -import { UnwrapReply, ArrayReply, BlobStringReply, BooleanReply, CommandArguments, DoubleReply, NullReply, NumberReply, RedisArgument, TuplesReply, MapReply, TypeMapping } from '../RESP/types'; +import { UnwrapReply, ArrayReply, BlobStringReply, BooleanReply, CommandArguments, DoubleReply, NullReply, NumberReply, RedisArgument, TuplesReply, MapReply, TypeMapping, Command } from '../RESP/types'; export function isNullReply(reply: unknown): reply is NullReply { return reply === null; @@ -107,6 +108,19 @@ export interface Stringable { toString(): string; } +export function transformTuplesToMap( + reply: UnwrapReply>, + func: (elem: any) => T, +) { + const message = Object.create(null); + + for (let i = 0; i < reply.length; i+= 2) { + message[reply[i].toString()] = func(reply[i + 1]); + } + + return message; +} + export function createTransformTuplesReplyFunc(preserve?: any, typeMapping?: TypeMapping) { return (reply: ArrayReply) => { return transformTuplesReply(reply, preserve, typeMapping); @@ -255,16 +269,16 @@ export function pushVariadicArgument( return args; } -export function pushOptionalVariadicArgument( - args: CommandArguments, +export function parseOptionalVariadicArgument( + parser: CommandParser, name: RedisArgument, value?: RedisVariadicArgument -): CommandArguments { - if (value === undefined) return args; +) { + if (value === undefined) return; - args.push(name); + parser.push(name); - return pushVariadicArgument(args, value); + parser.pushVariadicWithLength(value); } export enum CommandFlags { @@ -393,29 +407,27 @@ export interface SlotRange { end: number; } -function pushSlotRangeArguments( - args: CommandArguments, +function parseSlotRangeArguments( + parser: CommandParser, range: SlotRange ): void { - args.push( + parser.push( range.start.toString(), range.end.toString() ); } -export function pushSlotRangesArguments( - args: CommandArguments, +export function parseSlotRangesArguments( + parser: CommandParser, ranges: SlotRange | Array -): CommandArguments { +) { if (Array.isArray(ranges)) { for (const range of ranges) { - pushSlotRangeArguments(args, range); + parseSlotRangeArguments(parser, range); } } else { - pushSlotRangeArguments(args, ranges); + parseSlotRangeArguments(parser, ranges); } - - return args; } export type RawRangeReply = [ @@ -444,41 +456,36 @@ export type ZVariadicKeys = T | [T, ...Array]; export type ZKeys = ZVariadicKeys | ZVariadicKeys; -export function pushZKeysArguments( - args: CommandArguments, +export function parseZKeysArguments( + parser: CommandParser, keys: ZKeys ) { if (Array.isArray(keys)) { - args.push(keys.length.toString()); + parser.push(keys.length.toString()); if (keys.length) { if (isPlainKeys(keys)) { - args = args.concat(keys); + parser.pushKeys(keys); } else { - const start = args.length; - args[start + keys.length] = 'WEIGHTS'; for (let i = 0; i < keys.length; i++) { - const index = start + i; - args[index] = keys[i].key; - args[index + 1 + keys.length] = transformDoubleArgument(keys[i].weight); + parser.pushKey(keys[i].key) + } + parser.push('WEIGHTS'); + for (let i = 0; i < keys.length; i++) { + parser.push(transformDoubleArgument(keys[i].weight)); } } } } else { - args.push('1'); + parser.push('1'); if (isPlainKey(keys)) { - args.push(keys); + parser.pushKey(keys); } else { - args.push( - keys.key, - 'WEIGHTS', - transformDoubleArgument(keys.weight) - ); + parser.pushKey(keys.key); + parser.push('WEIGHTS', transformDoubleArgument(keys.weight)); } } - - return args; } function isPlainKey(key: RedisArgument | ZKeyAndWeight): key is RedisArgument { @@ -489,6 +496,22 @@ function isPlainKeys(keys: Array | Array): keys is return isPlainKey(keys[0]); } +export type Tail = T extends [infer Head, ...infer Tail] ? Tail : never; + +/** + * @deprecated + */ +export function parseArgs(command: Command, ...args: Array): CommandArguments { + const parser = new BasicCommandParser(); + command.parseCommand!(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + if (parser.preserve) { + redisArgs.preserve = parser.preserve; + } + return redisArgs; +} + export type StreamMessageRawReply = TuplesReply<[ id: BlobStringReply, message: ArrayReply diff --git a/packages/client/lib/multi-command.ts b/packages/client/lib/multi-command.ts index a3ff4c99407..3d45a02fb4d 100644 --- a/packages/client/lib/multi-command.ts +++ b/packages/client/lib/multi-command.ts @@ -16,6 +16,12 @@ export interface RedisMultiQueuedCommand { } export default class RedisMultiCommand { + private readonly typeMapping?: TypeMapping; + + constructor(typeMapping?: TypeMapping) { + this.typeMapping = typeMapping; + } + readonly queue: Array = []; readonly scriptsInUse = new Set(); @@ -46,7 +52,7 @@ export default class RedisMultiCommand { this.addCommand(redisArgs, transformReply); } - transformReplies(rawReplies: Array, typeMapping?: TypeMapping): Array { + transformReplies(rawReplies: Array): Array { const errorIndexes: Array = [], replies = rawReplies.map((reply, i) => { if (reply instanceof ErrorReply) { @@ -55,7 +61,7 @@ export default class RedisMultiCommand { } const { transformReply, args } = this.queue[i]; - return transformReply ? transformReply(reply, args.preserve, typeMapping) : reply; + return transformReply ? transformReply(reply, args.preserve, this.typeMapping) : reply; }); if (errorIndexes.length) throw new MultiErrorReply(replies, errorIndexes); diff --git a/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts b/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts index b260dcfba7d..84997ac7d8f 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_MASTER.ts @@ -1,9 +1,10 @@ import { RedisArgument, MapReply, BlobStringReply, Command } from '../../RESP/types'; +import { CommandParser } from '../../client/parser'; import { transformTuplesReply } from '../../commands/generic-transformers'; export default { - transformArguments(dbname: RedisArgument) { - return ['SENTINEL', 'MASTER', dbname]; + parseCommand(parser: CommandParser, dbname: RedisArgument) { + parser.push('SENTINEL', 'MASTER', dbname); }, transformReply: { 2: transformTuplesReply, diff --git a/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts b/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts index 14caecd924a..65f438de132 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_MONITOR.ts @@ -1,8 +1,9 @@ +import { CommandParser } from '../../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../../RESP/types'; export default { - transformArguments(dbname: RedisArgument, host: RedisArgument, port: RedisArgument, quorum: RedisArgument) { - return ['SENTINEL', 'MONITOR', dbname, host, port, quorum]; + parseCommand(parser: CommandParser, dbname: RedisArgument, host: RedisArgument, port: RedisArgument, quorum: RedisArgument) { + parser.push('SENTINEL', 'MONITOR', dbname, host, port, quorum); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts b/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts index 3d002896355..127449264d8 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_REPLICAS.ts @@ -1,9 +1,10 @@ +import { CommandParser } from '../../client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, MapReply, Command, TypeMapping, UnwrapReply } from '../../RESP/types'; import { transformTuplesReply } from '../../commands/generic-transformers'; export default { - transformArguments(dbname: RedisArgument) { - return ['SENTINEL', 'REPLICAS', dbname]; + parseCommand(parser: CommandParser, dbname: RedisArgument) { + parser.push('SENTINEL', 'REPLICAS', dbname); }, transformReply: { 2: (reply: ArrayReply>, preserve?: any, typeMapping?: TypeMapping) => { diff --git a/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts b/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts index 22c1e0123fc..4550b9498b3 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_SENTINELS.ts @@ -1,9 +1,10 @@ +import { CommandParser } from '../../client/parser'; import { RedisArgument, ArrayReply, MapReply, BlobStringReply, Command, TypeMapping, UnwrapReply } from '../../RESP/types'; import { transformTuplesReply } from '../../commands/generic-transformers'; export default { - transformArguments(dbname: RedisArgument) { - return ['SENTINEL', 'SENTINELS', dbname]; + parseCommand(parser: CommandParser, dbname: RedisArgument) { + parser.push('SENTINEL', 'SENTINELS', dbname); }, transformReply: { 2: (reply: ArrayReply>, preserve?: any, typeMapping?: TypeMapping) => { diff --git a/packages/client/lib/sentinel/commands/SENTINEL_SET.ts b/packages/client/lib/sentinel/commands/SENTINEL_SET.ts index 41037819869..b4e8f843ea6 100644 --- a/packages/client/lib/sentinel/commands/SENTINEL_SET.ts +++ b/packages/client/lib/sentinel/commands/SENTINEL_SET.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '../../client/parser'; import { RedisArgument, SimpleStringReply, Command } from '../../RESP/types'; export type SentinelSetOptions = Array<{ @@ -6,14 +7,12 @@ export type SentinelSetOptions = Array<{ }>; export default { - transformArguments(dbname: RedisArgument, options: SentinelSetOptions) { - const args = ['SENTINEL', 'SET', dbname]; + parseCommand(parser: CommandParser, dbname: RedisArgument, options: SentinelSetOptions) { + parser.push('SENTINEL', 'SET', dbname); for (const option of options) { - args.push(option.option, option.value); + parser.push(option.option, option.value); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index 1fba8d6b42f..be5522bdd8d 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -14,21 +14,10 @@ import { defineScript } from '../lua-script'; import { MATH_FUNCTION } from '../commands/FUNCTION_LOAD.spec'; import RedisBloomModules from '@redis/bloom'; import { RedisTcpSocketOptions } from '../client/socket'; +import { SQUARE_SCRIPT } from '../client/index.spec'; const execAsync = promisify(exec); -const SQUARE_SCRIPT = defineScript({ - SCRIPT: - `local number = redis.call('GET', KEYS[1]) - return number * number`, - NUMBER_OF_KEYS: 1, - FIRST_KEY_INDEX: 0, - transformArguments(key: string) { - return [key]; - }, - transformReply: undefined as unknown as () => NumberReply -}); - /* used to ensure test environment resets to normal state i.e. - all redis nodes are active and are part of the topology diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index b71514e9358..d25fa03e559 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'node:events'; -import { CommandArguments, RedisArgument, RedisFunctions, RedisModules, RedisScript, RedisScripts, ReplyUnion, RespVersions, TypeMapping } from '../RESP/types'; +import { CommandArguments, RedisFunctions, RedisModules, RedisScripts, ReplyUnion, RespVersions, TypeMapping } from '../RESP/types'; import RedisClient, { RedisClientOptions, RedisClientType } from '../client'; import { CommandOptions } from '../client/commands-queue'; import { attachConfig } from '../commander'; @@ -164,18 +164,6 @@ export class RedisSentinelClient< ); } - executeScript( - script: RedisScript, - isReadonly: boolean | undefined, - args: Array, - options?: CommandOptions - ) { - return this._execute( - isReadonly, - client => client.executeScript(script, args, options) - ); - } - /** * @internal */ @@ -440,18 +428,6 @@ export default class RedisSentinel< ); } - executeScript( - script: RedisScript, - isReadonly: boolean | undefined, - args: Array, - options?: CommandOptions - ) { - return this._execute( - isReadonly, - client => client.executeScript(script, args, options) - ); - } - /** * @internal */ diff --git a/packages/client/lib/sentinel/multi-commands.ts b/packages/client/lib/sentinel/multi-commands.ts index bf616370bf3..e70dc45c790 100644 --- a/packages/client/lib/sentinel/multi-commands.ts +++ b/packages/client/lib/sentinel/multi-commands.ts @@ -3,6 +3,8 @@ import RedisMultiCommand, { MULTI_REPLY, MultiReply, MultiReplyType } from '../m import { ReplyWithTypeMapping, CommandReply, Command, CommandArguments, CommanderConfig, RedisFunctions, RedisModules, RedisScripts, RespVersions, TransformReply, RedisScript, RedisFunction, TypeMapping } from '../RESP/types'; import { attachConfig, functionArgumentsPrefix, getTransformReply } from '../commander'; import { RedisSentinelType } from './types'; +import { BasicCommandParser } from '../client/parser'; +import { Tail } from '../commands/generic-transformers'; type CommandSignature< REPLIES extends Array, @@ -12,7 +14,7 @@ type CommandSignature< S extends RedisScripts, RESP extends RespVersions, TYPE_MAPPING extends TypeMapping -> = (...args: Parameters) => RedisSentinelMultiCommandType< +> = (...args: Tail>) => RedisSentinelMultiCommandType< [...REPLIES, ReplyWithTypeMapping, TYPE_MAPPING>], M, F, @@ -87,8 +89,14 @@ export type RedisSentinelMultiCommandType< export default class RedisSentinelMultiCommand { private static _createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + return function (this: RedisSentinelMultiCommand, ...args: Array) { - const redisArgs = command.transformArguments(...args); + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + return this.addCommand( command.IS_READ_ONLY, redisArgs, @@ -99,8 +107,14 @@ export default class RedisSentinelMultiCommand { private static _createModuleCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + return function (this: { _self: RedisSentinelMultiCommand }, ...args: Array) { - const redisArgs = command.transformArguments(...args); + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + return this._self.addCommand( command.IS_READ_ONLY, redisArgs, @@ -110,12 +124,17 @@ export default class RedisSentinelMultiCommand { } private static _createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { - const prefix = functionArgumentsPrefix(name, fn), - transformReply = getTransformReply(fn, resp); + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); + return function (this: { _self: RedisSentinelMultiCommand }, ...args: Array) { - const fnArgs = fn.transformArguments(...args); - const redisArgs: CommandArguments = prefix.concat(fnArgs); - redisArgs.preserve = fnArgs.preserve; + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); + + const redisArgs: CommandArguments = parser.redisArgs; + redisArgs.preserve = parser.preserve; + return this._self.addCommand( fn.IS_READ_ONLY, redisArgs, @@ -126,17 +145,20 @@ export default class RedisSentinelMultiCommand { private static _createScriptCommand(script: RedisScript, resp: RespVersions) { const transformReply = getTransformReply(script, resp); + return function (this: RedisSentinelMultiCommand, ...args: Array) { - const scriptArgs = script.transformArguments(...args); - this._setState( - script.IS_READ_ONLY - ); - this._multi.addScript( + const parser = new BasicCommandParser(); + script.parseCommand(parser, ...args); + + const scriptArgs: CommandArguments = parser.redisArgs; + scriptArgs.preserve = parser.preserve; + + return this.#addScript( + script.IS_READ_ONLY, script, scriptArgs, transformReply ); - return this; }; } @@ -157,20 +179,19 @@ export default class RedisSentinelMultiCommand { }); } - private readonly _multi = new RedisMultiCommand(); - private readonly _sentinel: RedisSentinelType - private _isReadonly: boolean | undefined = true; - private readonly _typeMapping?: TypeMapping; + readonly #multi = new RedisMultiCommand(); + readonly #sentinel: RedisSentinelType + #isReadonly: boolean | undefined = true; constructor(sentinel: RedisSentinelType, typeMapping: TypeMapping) { - this._sentinel = sentinel; - this._typeMapping = typeMapping; + this.#multi = new RedisMultiCommand(typeMapping); + this.#sentinel = sentinel; } - private _setState( + #setState( isReadonly: boolean | undefined, ) { - this._isReadonly &&= isReadonly; + this.#isReadonly &&= isReadonly; } addCommand( @@ -178,20 +199,31 @@ export default class RedisSentinelMultiCommand { args: CommandArguments, transformReply?: TransformReply ) { - this._setState(isReadonly); - this._multi.addCommand(args, transformReply); + this.#setState(isReadonly); + this.#multi.addCommand(args, transformReply); + return this; + } + + #addScript( + isReadonly: boolean | undefined, + script: RedisScript, + args: CommandArguments, + transformReply?: TransformReply + ) { + this.#setState(isReadonly); + this.#multi.addScript(script, args, transformReply); + return this; } async exec(execAsPipeline = false) { if (execAsPipeline) return this.execAsPipeline(); - return this._multi.transformReplies( - await this._sentinel._executeMulti( - this._isReadonly, - this._multi.queue - ), - this._typeMapping + return this.#multi.transformReplies( + await this.#sentinel._executeMulti( + this.#isReadonly, + this.#multi.queue + ) ) as MultiReplyType; } @@ -202,14 +234,13 @@ export default class RedisSentinelMultiCommand { } async execAsPipeline() { - if (this._multi.queue.length === 0) return [] as MultiReplyType; - - return this._multi.transformReplies( - await this._sentinel._executePipeline( - this._isReadonly, - this._multi.queue - ), - this._typeMapping + if (this.#multi.queue.length === 0) return [] as MultiReplyType; + + return this.#multi.transformReplies( + await this.#sentinel._executePipeline( + this.#isReadonly, + this.#multi.queue + ) ) as MultiReplyType; } diff --git a/packages/client/lib/sentinel/utils.ts b/packages/client/lib/sentinel/utils.ts index b4d430b1b44..90b789ddca9 100644 --- a/packages/client/lib/sentinel/utils.ts +++ b/packages/client/lib/sentinel/utils.ts @@ -1,3 +1,4 @@ +import { BasicCommandParser } from '../client/parser'; import { ArrayReply, Command, RedisFunction, RedisScript, RespVersions, UnwrapReply } from '../RESP/types'; import { RedisSocketOptions, RedisTcpSocketOptions } from '../client/socket'; import { functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; @@ -38,77 +39,60 @@ export function clientSocketToNode(socket: RedisSocketOptions): RedisNode { export function createCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + return async function (this: T, ...args: Array) { - const redisArgs = command.transformArguments(...args); - const typeMapping = this._self.commandOptions?.typeMapping; + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - const reply = await this._self.sendCommand( + return this._self._execute( command.IS_READ_ONLY, - redisArgs, - this._self.commandOptions + client => client._executeCommand(command, parser, this.commandOptions, transformReply) ); - - return transformReply ? - transformReply(reply, redisArgs.preserve, typeMapping) : - reply; }; } export function createFunctionCommand(name: string, fn: RedisFunction, resp: RespVersions) { - const prefix = functionArgumentsPrefix(name, fn), - transformReply = getTransformReply(fn, resp); + const prefix = functionArgumentsPrefix(name, fn); + const transformReply = getTransformReply(fn, resp); + return async function (this: T, ...args: Array) { - const fnArgs = fn.transformArguments(...args); - const redisArgs = prefix.concat(fnArgs); - const typeMapping = this._self._self.commandOptions?.typeMapping; + const parser = new BasicCommandParser(); + parser.push(...prefix); + fn.parseCommand(parser, ...args); - const reply = await this._self._self.sendCommand( + return this._self._execute( fn.IS_READ_ONLY, - redisArgs, - this._self._self.commandOptions + client => client._executeCommand(fn, parser, this._self.commandOptions, transformReply) ); - - return transformReply ? - transformReply(reply, fnArgs.preserve, typeMapping) : - reply; } }; export function createModuleCommand(command: Command, resp: RespVersions) { const transformReply = getTransformReply(command, resp); + return async function (this: T, ...args: Array) { - const redisArgs = command.transformArguments(...args); - const typeMapping = this._self._self.commandOptions?.typeMapping; + const parser = new BasicCommandParser(); + command.parseCommand(parser, ...args); - const reply = await this._self._self.sendCommand( + return this._self._execute( command.IS_READ_ONLY, - redisArgs, - this._self._self.commandOptions + client => client._executeCommand(command, parser, this._self.commandOptions, transformReply) ); - - return transformReply ? - transformReply(reply, redisArgs.preserve, typeMapping) : - reply; } }; export function createScriptCommand(script: RedisScript, resp: RespVersions) { - const prefix = scriptArgumentsPrefix(script), - transformReply = getTransformReply(script, resp); + const prefix = scriptArgumentsPrefix(script); + const transformReply = getTransformReply(script, resp); + return async function (this: T, ...args: Array) { - const scriptArgs = script.transformArguments(...args); - const redisArgs = prefix.concat(scriptArgs); - const typeMapping = this._self.commandOptions?.typeMapping; + const parser = new BasicCommandParser(); + parser.push(...prefix); + script.parseCommand(parser, ...args); - const reply = await this._self.executeScript( - script, + return this._self._execute( script.IS_READ_ONLY, - redisArgs, - this._self.commandOptions + client => client._executeScript(script, parser, this.commandOptions, transformReply) ); - - return transformReply ? - transformReply(reply, scriptArgs.preserve, typeMapping) : - reply; }; } diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 29eb03cb73d..083c9127e5b 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -1,6 +1,8 @@ import TestUtils from '@redis/test-utils'; import { SinonSpy } from 'sinon'; import { setTimeout } from 'node:timers/promises'; +import { Command } from './RESP/types'; +import { BasicCommandParser } from './client/parser'; const utils = new TestUtils({ dockerImageName: 'redis/redis-stack', @@ -67,3 +69,9 @@ export const BLOCKING_MIN_VALUE = ( utils.isVersionGreaterThan([6]) ? 0.01 : 1 ); + +export function parseFirstKey(command: Command, ...args: Array) { + const parser = new BasicCommandParser(); + command.parseCommand!(parser, ...args); + return parser.firstKey; +} diff --git a/packages/graph/lib/commands/CONFIG_GET.spec.ts b/packages/graph/lib/commands/CONFIG_GET.spec.ts index 42c7739f5d7..9a427867c63 100644 --- a/packages/graph/lib/commands/CONFIG_GET.spec.ts +++ b/packages/graph/lib/commands/CONFIG_GET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CONFIG_GET from './CONFIG_GET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('GRAPH.CONFIG GET', () => { it('transformArguments', () => { assert.deepEqual( - CONFIG_GET.transformArguments('TIMEOUT'), + parseArgs(CONFIG_GET, 'TIMEOUT'), ['GRAPH.CONFIG', 'GET', 'TIMEOUT'] ); }); diff --git a/packages/graph/lib/commands/CONFIG_GET.ts b/packages/graph/lib/commands/CONFIG_GET.ts index c7ed037e1a1..8ff289876d3 100644 --- a/packages/graph/lib/commands/CONFIG_GET.ts +++ b/packages/graph/lib/commands/CONFIG_GET.ts @@ -1,4 +1,5 @@ -import { RedisArgument, TuplesReply, ArrayReply, BlobStringReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, TuplesReply, ArrayReply, BlobStringReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; type ConfigItemReply = TuplesReply<[ configKey: BlobStringReply, @@ -6,10 +7,10 @@ type ConfigItemReply = TuplesReply<[ ]>; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(configKey: RedisArgument) { - return ['GRAPH.CONFIG', 'GET', configKey]; + parseCommand(parser: CommandParser, configKey: RedisArgument) { + parser.push('GRAPH.CONFIG', 'GET', configKey); }, transformReply: undefined as unknown as () => ConfigItemReply | ArrayReply } as const satisfies Command; diff --git a/packages/graph/lib/commands/CONFIG_SET.spec.ts b/packages/graph/lib/commands/CONFIG_SET.spec.ts index 5ed51e78a29..ae6e296699c 100644 --- a/packages/graph/lib/commands/CONFIG_SET.spec.ts +++ b/packages/graph/lib/commands/CONFIG_SET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CONFIG_SET from './CONFIG_SET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('GRAPH.CONFIG SET', () => { it('transformArguments', () => { assert.deepEqual( - CONFIG_SET.transformArguments('TIMEOUT', 0), + parseArgs(CONFIG_SET, 'TIMEOUT', 0), ['GRAPH.CONFIG', 'SET', 'TIMEOUT', '0'] ); }); diff --git a/packages/graph/lib/commands/CONFIG_SET.ts b/packages/graph/lib/commands/CONFIG_SET.ts index ba23ac2f1a7..b37d8690bfa 100644 --- a/packages/graph/lib/commands/CONFIG_SET.ts +++ b/packages/graph/lib/commands/CONFIG_SET.ts @@ -1,15 +1,11 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(configKey: RedisArgument, value: number) { - return [ - 'GRAPH.CONFIG', - 'SET', - configKey, - value.toString() - ]; + parseCommand(parser: CommandParser, configKey: RedisArgument, value: number) { + parser.push('GRAPH.CONFIG', 'SET', configKey, value.toString()); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/graph/lib/commands/DELETE.spec.ts b/packages/graph/lib/commands/DELETE.spec.ts index 6fe24fd827a..5977c646307 100644 --- a/packages/graph/lib/commands/DELETE.spec.ts +++ b/packages/graph/lib/commands/DELETE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DELETE from './DELETE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('GRAPH.DELETE', () => { it('transformArguments', () => { assert.deepEqual( - DELETE.transformArguments('key'), + parseArgs(DELETE, 'key'), ['GRAPH.DELETE', 'key'] ); }); diff --git a/packages/graph/lib/commands/DELETE.ts b/packages/graph/lib/commands/DELETE.ts index f5f99fb92cc..2e1f9ff003c 100644 --- a/packages/graph/lib/commands/DELETE.ts +++ b/packages/graph/lib/commands/DELETE.ts @@ -1,10 +1,11 @@ -import { RedisArgument, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument) { - return ['GRAPH.DELETE', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('GRAPH.DELETE'); + parser.pushKey(key); }, transformReply: undefined as unknown as () => BlobStringReply } as const satisfies Command; diff --git a/packages/graph/lib/commands/EXPLAIN.spec.ts b/packages/graph/lib/commands/EXPLAIN.spec.ts index 04bf838a4de..28f30cd17b3 100644 --- a/packages/graph/lib/commands/EXPLAIN.spec.ts +++ b/packages/graph/lib/commands/EXPLAIN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import EXPLAIN from './EXPLAIN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('GRAPH.EXPLAIN', () => { it('transformArguments', () => { assert.deepEqual( - EXPLAIN.transformArguments('key', 'RETURN 0'), + parseArgs(EXPLAIN, 'key', 'RETURN 0'), ['GRAPH.EXPLAIN', 'key', 'RETURN 0'] ); }); diff --git a/packages/graph/lib/commands/EXPLAIN.ts b/packages/graph/lib/commands/EXPLAIN.ts index 99a73bf04bf..c690450a10f 100644 --- a/packages/graph/lib/commands/EXPLAIN.ts +++ b/packages/graph/lib/commands/EXPLAIN.ts @@ -1,10 +1,12 @@ -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, query: RedisArgument) { - return ['GRAPH.EXPLAIN', key, query]; + parseCommand(parser: CommandParser, key: RedisArgument, query: RedisArgument) { + parser.push('GRAPH.EXPLAIN'); + parser.pushKey(key); + parser.push(query); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/graph/lib/commands/LIST.spec.ts b/packages/graph/lib/commands/LIST.spec.ts index 36745efc470..19f18a0e30b 100644 --- a/packages/graph/lib/commands/LIST.spec.ts +++ b/packages/graph/lib/commands/LIST.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import LIST from './LIST'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('GRAPH.LIST', () => { it('transformArguments', () => { assert.deepEqual( - LIST.transformArguments(), + parseArgs(LIST), ['GRAPH.LIST'] ); }); diff --git a/packages/graph/lib/commands/LIST.ts b/packages/graph/lib/commands/LIST.ts index 01a868854be..4fe66d748fa 100644 --- a/packages/graph/lib/commands/LIST.ts +++ b/packages/graph/lib/commands/LIST.ts @@ -1,10 +1,11 @@ -import { ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['GRAPH.LIST']; + parseCommand(parser: CommandParser) { + parser.push('GRAPH.LIST'); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/graph/lib/commands/PROFILE.spec.ts b/packages/graph/lib/commands/PROFILE.spec.ts index a758365d56e..7f16fd3ba58 100644 --- a/packages/graph/lib/commands/PROFILE.spec.ts +++ b/packages/graph/lib/commands/PROFILE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PROFILE from './PROFILE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('GRAPH.PROFILE', () => { it('transformArguments', () => { assert.deepEqual( - PROFILE.transformArguments('key', 'RETURN 0'), + parseArgs(PROFILE, 'key', 'RETURN 0'), ['GRAPH.PROFILE', 'key', 'RETURN 0'] ); }); diff --git a/packages/graph/lib/commands/PROFILE.ts b/packages/graph/lib/commands/PROFILE.ts index 2aa1e83dfb0..fba0973baaf 100644 --- a/packages/graph/lib/commands/PROFILE.ts +++ b/packages/graph/lib/commands/PROFILE.ts @@ -1,10 +1,12 @@ -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, query: RedisArgument) { - return ['GRAPH.PROFILE', key, query]; + parseCommand(parser: CommandParser, key: RedisArgument, query: RedisArgument) { + parser.push('GRAPH.PROFILE'); + parser.pushKey(key); + parser.push(query); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/graph/lib/commands/QUERY.spec.ts b/packages/graph/lib/commands/QUERY.spec.ts index 62c9bcaaefe..28c2189645b 100644 --- a/packages/graph/lib/commands/QUERY.spec.ts +++ b/packages/graph/lib/commands/QUERY.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import QUERY from './QUERY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('GRAPH.QUERY', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - QUERY.transformArguments('key', 'query'), + parseArgs(QUERY, 'key', 'query'), ['GRAPH.QUERY', 'key', 'query'] ); }); @@ -14,7 +15,7 @@ describe('GRAPH.QUERY', () => { describe('params', () => { it('all types', () => { assert.deepEqual( - QUERY.transformArguments('key', 'query', { + parseArgs(QUERY, 'key', 'query', { params: { null: null, string: '"\\', @@ -30,7 +31,7 @@ describe('GRAPH.QUERY', () => { it('TypeError', () => { assert.throws(() => { - QUERY.transformArguments('key', 'query', { + parseArgs(QUERY, 'key', 'query', { params: { a: Symbol() } @@ -41,7 +42,7 @@ describe('GRAPH.QUERY', () => { it('TIMEOUT', () => { assert.deepEqual( - QUERY.transformArguments('key', 'query', { + parseArgs(QUERY, 'key', 'query', { TIMEOUT: 1 }), ['GRAPH.QUERY', 'key', 'query', 'TIMEOUT', '1'] @@ -50,7 +51,7 @@ describe('GRAPH.QUERY', () => { it('compact', () => { assert.deepEqual( - QUERY.transformArguments('key', 'query', undefined, true), + parseArgs(QUERY, 'key', 'query', undefined, true), ['GRAPH.QUERY', 'key', 'query', '--compact'] ); }); diff --git a/packages/graph/lib/commands/QUERY.ts b/packages/graph/lib/commands/QUERY.ts index 8a052354610..c96c00ff325 100644 --- a/packages/graph/lib/commands/QUERY.ts +++ b/packages/graph/lib/commands/QUERY.ts @@ -1,4 +1,5 @@ -import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, NullReply, TuplesReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, NullReply, TuplesReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; type Headers = ArrayReply; @@ -25,30 +26,28 @@ export interface QueryOptions { TIMEOUT?: number; } -export function transformQueryArguments( +export function parseQueryArguments( command: RedisArgument, + parser: CommandParser, graph: RedisArgument, query: RedisArgument, options?: QueryOptions, compact?: boolean ) { - const args = [ - command, - graph, - options?.params ? - `CYPHER ${queryParamsToString(options.params)} ${query}` : - query - ]; + parser.push(command); + parser.pushKey(graph); + const param = options?.params ? + `CYPHER ${queryParamsToString(options.params)} ${query}` : + query; + parser.push(param); if (options?.TIMEOUT !== undefined) { - args.push('TIMEOUT', options.TIMEOUT.toString()); + parser.push('TIMEOUT', options.TIMEOUT.toString()); } if (compact) { - args.push('--compact'); + parser.push('--compact'); } - - return args; } function queryParamsToString(params: QueryParams) { @@ -85,9 +84,8 @@ function queryParamToString(param: QueryParam): string { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments: transformQueryArguments.bind(undefined, 'GRAPH.QUERY'), + parseCommand: parseQueryArguments.bind(undefined, 'GRAPH.QUERY'), transformReply(reply: UnwrapReply) { return reply.length === 1 ? { headers: undefined, diff --git a/packages/graph/lib/commands/RO_QUERY.spec.ts b/packages/graph/lib/commands/RO_QUERY.spec.ts index 13829543552..fa9459cf642 100644 --- a/packages/graph/lib/commands/RO_QUERY.spec.ts +++ b/packages/graph/lib/commands/RO_QUERY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RO_QUERY from './RO_QUERY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('GRAPH.RO_QUERY', () => { it('transformArguments', () => { assert.deepEqual( - RO_QUERY.transformArguments('key', 'query'), + parseArgs(RO_QUERY, 'key', 'query'), ['GRAPH.RO_QUERY', 'key', 'query'] ); }); diff --git a/packages/graph/lib/commands/RO_QUERY.ts b/packages/graph/lib/commands/RO_QUERY.ts index 5987f511b7d..98668675d21 100644 --- a/packages/graph/lib/commands/RO_QUERY.ts +++ b/packages/graph/lib/commands/RO_QUERY.ts @@ -1,9 +1,8 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; -import QUERY, { transformQueryArguments } from './QUERY'; +import { Command } from '@redis/client/lib/RESP/types'; +import QUERY, { parseQueryArguments } from './QUERY'; export default { - FIRST_KEY_INDEX: QUERY.FIRST_KEY_INDEX, IS_READ_ONLY: true, - transformArguments: transformQueryArguments.bind(undefined, 'GRAPH.RO_QUERY'), + parseCommand: parseQueryArguments.bind(undefined, 'GRAPH.RO_QUERY'), transformReply: QUERY.transformReply } as const satisfies Command; diff --git a/packages/graph/lib/commands/SLOWLOG.spec.ts b/packages/graph/lib/commands/SLOWLOG.spec.ts index c1c77286a26..b991d6e7b96 100644 --- a/packages/graph/lib/commands/SLOWLOG.spec.ts +++ b/packages/graph/lib/commands/SLOWLOG.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SLOWLOG from './SLOWLOG'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('GRAPH.SLOWLOG', () => { it('transformArguments', () => { assert.deepEqual( - SLOWLOG.transformArguments('key'), + parseArgs(SLOWLOG, 'key'), ['GRAPH.SLOWLOG', 'key'] ); }); diff --git a/packages/graph/lib/commands/SLOWLOG.ts b/packages/graph/lib/commands/SLOWLOG.ts index 52927f6040b..bba615efb21 100644 --- a/packages/graph/lib/commands/SLOWLOG.ts +++ b/packages/graph/lib/commands/SLOWLOG.ts @@ -1,4 +1,5 @@ -import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; type SlowLogRawReply = ArrayReply>; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['GRAPH.SLOWLOG', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('GRAPH.SLOWLOG'); + parser.pushKey(key); }, transformReply(reply: UnwrapReply) { return reply.map(log => { diff --git a/packages/graph/lib/commands/index.ts b/packages/graph/lib/commands/index.ts index e93356aa951..362d98f970d 100644 --- a/packages/graph/lib/commands/index.ts +++ b/packages/graph/lib/commands/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; +import type { RedisCommands } from '@redis/client/lib/RESP/types'; import CONFIG_GET from './CONFIG_GET'; import CONFIG_SET from './CONFIG_SET';; import DELETE from './DELETE'; diff --git a/packages/graph/lib/graph.ts b/packages/graph/lib/graph.ts index 348c8b7155f..cacdcd5aec0 100644 --- a/packages/graph/lib/graph.ts +++ b/packages/graph/lib/graph.ts @@ -1,5 +1,5 @@ import { RedisClientType } from '@redis/client'; -import { RedisArgument, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, RedisFunctions, RedisScripts } from '@redis/client/lib/RESP/types'; import QUERY, { QueryOptions } from './commands/QUERY'; interface GraphMetadata { diff --git a/packages/json/lib/commands/ARRAPPEND.spec.ts b/packages/json/lib/commands/ARRAPPEND.spec.ts index 3bdd967e237..b2c22e0b9c0 100644 --- a/packages/json/lib/commands/ARRAPPEND.spec.ts +++ b/packages/json/lib/commands/ARRAPPEND.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ARRAPPEND from './ARRAPPEND'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.ARRAPPEND', () => { describe('transformArguments', () => { it('single element', () => { assert.deepEqual( - ARRAPPEND.transformArguments('key', '$', 'value'), + parseArgs(ARRAPPEND, 'key', '$', 'value'), ['JSON.ARRAPPEND', 'key', '$', '"value"'] ); }); it('multiple elements', () => { assert.deepEqual( - ARRAPPEND.transformArguments('key', '$', 1, 2), + parseArgs(ARRAPPEND, 'key', '$', 1, 2), ['JSON.ARRAPPEND', 'key', '$', '1', '2'] ); }); diff --git a/packages/json/lib/commands/ARRAPPEND.ts b/packages/json/lib/commands/ARRAPPEND.ts index 6f486a301d7..ee79119b9f7 100644 --- a/packages/json/lib/commands/ARRAPPEND.ts +++ b/packages/json/lib/commands/ARRAPPEND.ts @@ -1,27 +1,23 @@ +import { CommandParser } from '@redis/client/lib/client/parser'; import { RedisJSON, transformRedisJsonArgument } from '.'; -import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, path: RedisArgument, json: RedisJSON, ...jsons: Array ) { - const args = new Array(4 + jsons.length); - args[0] = 'JSON.ARRAPPEND'; - args[1] = key; - args[2] = path; - args[3] = transformRedisJsonArgument(json); + parser.push('JSON.ARRAPPEND'); + parser.pushKey(key); + parser.push(path, transformRedisJsonArgument(json)); - let argsIndex = 4; for (let i = 0; i < jsons.length; i++) { - args[argsIndex++] = transformRedisJsonArgument(jsons[i]); + parser.push(transformRedisJsonArgument(jsons[i])); } - - return args; }, transformReply: undefined as unknown as () => NumberReply | ArrayReply } as const satisfies Command; diff --git a/packages/json/lib/commands/ARRINDEX.spec.ts b/packages/json/lib/commands/ARRINDEX.spec.ts index cb946b62515..3c1377354f1 100644 --- a/packages/json/lib/commands/ARRINDEX.spec.ts +++ b/packages/json/lib/commands/ARRINDEX.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ARRINDEX from './ARRINDEX'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.ARRINDEX', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ARRINDEX.transformArguments('key', '$', 'value'), + parseArgs(ARRINDEX, 'key', '$', 'value'), ['JSON.ARRINDEX', 'key', '$', '"value"'] ); }); @@ -14,7 +15,7 @@ describe('JSON.ARRINDEX', () => { describe('with range', () => { it('start only', () => { assert.deepEqual( - ARRINDEX.transformArguments('key', '$', 'value', { + parseArgs(ARRINDEX, 'key', '$', 'value', { range: { start: 0 } @@ -25,7 +26,7 @@ describe('JSON.ARRINDEX', () => { it('with start and stop', () => { assert.deepEqual( - ARRINDEX.transformArguments('key', '$', 'value', { + parseArgs(ARRINDEX, 'key', '$', 'value', { range: { start: 0, stop: 1 diff --git a/packages/json/lib/commands/ARRINDEX.ts b/packages/json/lib/commands/ARRINDEX.ts index 77c54b92522..d0533862c60 100644 --- a/packages/json/lib/commands/ARRINDEX.ts +++ b/packages/json/lib/commands/ARRINDEX.ts @@ -1,4 +1,5 @@ -import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; export interface JsonArrIndexOptions { @@ -9,25 +10,25 @@ export interface JsonArrIndexOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, path: RedisArgument, json: RedisJSON, options?: JsonArrIndexOptions ) { - const args = ['JSON.ARRINDEX', key, path, transformRedisJsonArgument(json)]; + parser.push('JSON.ARRINDEX'); + parser.pushKey(key); + parser.push(path, transformRedisJsonArgument(json)); if (options?.range) { - args.push(options.range.start.toString()); + parser.push(options.range.start.toString()); if (options.range.stop !== undefined) { - args.push(options.range.stop.toString()); + parser.push(options.range.stop.toString()); } } - - return args; }, transformReply: undefined as unknown as () => NumberReply | ArrayReply } as const satisfies Command; diff --git a/packages/json/lib/commands/ARRINSERT.spec.ts b/packages/json/lib/commands/ARRINSERT.spec.ts index efa824b3738..bf9c8a2a051 100644 --- a/packages/json/lib/commands/ARRINSERT.spec.ts +++ b/packages/json/lib/commands/ARRINSERT.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ARRINSERT from './ARRINSERT'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.ARRINSERT', () => { describe('transformArguments', () => { it('single element', () => { assert.deepEqual( - ARRINSERT.transformArguments('key', '$', 0, 'value'), + parseArgs(ARRINSERT, 'key', '$', 0, 'value'), ['JSON.ARRINSERT', 'key', '$', '0', '"value"'] ); }); it('multiple elements', () => { assert.deepEqual( - ARRINSERT.transformArguments('key', '$', 0, '1', '2'), + parseArgs(ARRINSERT, 'key', '$', 0, '1', '2'), ['JSON.ARRINSERT', 'key', '$', '0', '"1"', '"2"'] ); }); diff --git a/packages/json/lib/commands/ARRINSERT.ts b/packages/json/lib/commands/ARRINSERT.ts index c0891884725..7a55577c9d6 100644 --- a/packages/json/lib/commands/ARRINSERT.ts +++ b/packages/json/lib/commands/ARRINSERT.ts @@ -1,29 +1,24 @@ -import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, path: RedisArgument, index: number, json: RedisJSON, ...jsons: Array ) { - const args = new Array(4 + jsons.length); - args[0] = 'JSON.ARRINSERT'; - args[1] = key; - args[2] = path; - args[3] = index.toString(); - args[4] = transformRedisJsonArgument(json); + parser.push('JSON.ARRINSERT'); + parser.pushKey(key); + parser.push(path, index.toString(), transformRedisJsonArgument(json)); - let argsIndex = 5; for (let i = 0; i < jsons.length; i++) { - args[argsIndex++] = transformRedisJsonArgument(jsons[i]); + parser.push(transformRedisJsonArgument(jsons[i])); } - - return args; }, transformReply: undefined as unknown as () => NumberReply | ArrayReply } as const satisfies Command; diff --git a/packages/json/lib/commands/ARRLEN.spec.ts b/packages/json/lib/commands/ARRLEN.spec.ts index 5ecb01b2ce5..dcf7d35acb0 100644 --- a/packages/json/lib/commands/ARRLEN.spec.ts +++ b/packages/json/lib/commands/ARRLEN.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ARRLEN from './ARRLEN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.ARRLEN', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ARRLEN.transformArguments('key'), + parseArgs(ARRLEN, 'key'), ['JSON.ARRLEN', 'key'] ); }); it('with path', () => { assert.deepEqual( - ARRLEN.transformArguments('key', { + parseArgs(ARRLEN, 'key', { path: '$' }), ['JSON.ARRLEN', 'key', '$'] diff --git a/packages/json/lib/commands/ARRLEN.ts b/packages/json/lib/commands/ARRLEN.ts index d30032c7d86..26accf8df99 100644 --- a/packages/json/lib/commands/ARRLEN.ts +++ b/packages/json/lib/commands/ARRLEN.ts @@ -1,20 +1,18 @@ -import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/lib/RESP/types'; export interface JsonArrLenOptions { path?: RedisArgument; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, options?: JsonArrLenOptions) { - const args = ['JSON.ARRLEN', key]; - + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonArrLenOptions) { + parser.push('JSON.ARRLEN'); + parser.pushKey(key); if (options?.path !== undefined) { - args.push(options.path); + parser.push(options.path); } - - return args; }, transformReply: undefined as unknown as () => NumberReply | ArrayReply } as const satisfies Command; diff --git a/packages/json/lib/commands/ARRPOP.spec.ts b/packages/json/lib/commands/ARRPOP.spec.ts index 1b069ba3928..f823e7fc08a 100644 --- a/packages/json/lib/commands/ARRPOP.spec.ts +++ b/packages/json/lib/commands/ARRPOP.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ARRPOP from './ARRPOP'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.ARRPOP', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - ARRPOP.transformArguments('key'), + parseArgs(ARRPOP, 'key'), ['JSON.ARRPOP', 'key'] ); }); it('with path', () => { assert.deepEqual( - ARRPOP.transformArguments('key', { + parseArgs(ARRPOP, 'key', { path: '$' }), ['JSON.ARRPOP', 'key', '$'] @@ -22,7 +23,7 @@ describe('JSON.ARRPOP', () => { it('with path and index', () => { assert.deepEqual( - ARRPOP.transformArguments('key', { + parseArgs(ARRPOP, 'key', { path: '$', index: 0 }), diff --git a/packages/json/lib/commands/ARRPOP.ts b/packages/json/lib/commands/ARRPOP.ts index 4eafe9fdde4..375668471d1 100644 --- a/packages/json/lib/commands/ARRPOP.ts +++ b/packages/json/lib/commands/ARRPOP.ts @@ -1,5 +1,6 @@ -import { RedisArgument, ArrayReply, NullReply, BlobStringReply, Command, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; -import { isArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, NullReply, BlobStringReply, Command, UnwrapReply } from '@redis/client/lib/RESP/types'; +import { isArrayReply } from '@redis/client/lib/commands/generic-transformers'; import { transformRedisJsonNullReply } from '.'; export interface RedisArrPopOptions { @@ -8,20 +9,18 @@ export interface RedisArrPopOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: RedisArrPopOptions) { - const args = ['JSON.ARRPOP', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: RedisArrPopOptions) { + parser.push('JSON.ARRPOP'); + parser.pushKey(key); if (options) { - args.push(options.path); + parser.push(options.path); if (options.index !== undefined) { - args.push(options.index.toString()); + parser.push(options.index.toString()); } } - - return args; }, transformReply(reply: NullReply | BlobStringReply | ArrayReply) { return isArrayReply(reply) ? diff --git a/packages/json/lib/commands/ARRTRIM.spec.ts b/packages/json/lib/commands/ARRTRIM.spec.ts index 4c2f72aaa50..e346716e8df 100644 --- a/packages/json/lib/commands/ARRTRIM.spec.ts +++ b/packages/json/lib/commands/ARRTRIM.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ARRTRIM from './ARRTRIM'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.ARRTRIM', () => { it('transformArguments', () => { assert.deepEqual( - ARRTRIM.transformArguments('key', '$', 0, 1), + parseArgs(ARRTRIM, 'key', '$', 0, 1), ['JSON.ARRTRIM', 'key', '$', '0', '1'] ); }); diff --git a/packages/json/lib/commands/ARRTRIM.ts b/packages/json/lib/commands/ARRTRIM.ts index ab31f159491..e7cde0dc172 100644 --- a/packages/json/lib/commands/ARRTRIM.ts +++ b/packages/json/lib/commands/ARRTRIM.ts @@ -1,10 +1,12 @@ -import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, path: RedisArgument, start: number, stop: number) { - return ['JSON.ARRTRIM', key, path, start.toString(), stop.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, start: number, stop: number) { + parser.push('JSON.ARRTRIM'); + parser.pushKey(key); + parser.push(path, start.toString(), stop.toString()); }, transformReply: undefined as unknown as () => NumberReply | ArrayReply } as const satisfies Command; diff --git a/packages/json/lib/commands/CLEAR.spec.ts b/packages/json/lib/commands/CLEAR.spec.ts index 983e6bec2d9..c1786cc1dde 100644 --- a/packages/json/lib/commands/CLEAR.spec.ts +++ b/packages/json/lib/commands/CLEAR.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CLEAR from './CLEAR'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.CLEAR', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - CLEAR.transformArguments('key'), + parseArgs(CLEAR, 'key'), ['JSON.CLEAR', 'key'] ); }); it('with path', () => { assert.deepEqual( - CLEAR.transformArguments('key', { + parseArgs(CLEAR, 'key', { path: '$' }), ['JSON.CLEAR', 'key', '$'] diff --git a/packages/json/lib/commands/CLEAR.ts b/packages/json/lib/commands/CLEAR.ts index 23e86d900e7..65d69ef18e5 100644 --- a/packages/json/lib/commands/CLEAR.ts +++ b/packages/json/lib/commands/CLEAR.ts @@ -1,20 +1,19 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; export interface JsonClearOptions { path?: RedisArgument; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: JsonClearOptions) { - const args = ['JSON.CLEAR', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonClearOptions) { + parser.push('JSON.CLEAR'); + parser.pushKey(key); if (options?.path !== undefined) { - args.push(options.path); + parser.push(options.path); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/json/lib/commands/DEBUG_MEMORY.spec.ts b/packages/json/lib/commands/DEBUG_MEMORY.spec.ts index c41d07cb27e..09c29328d8e 100644 --- a/packages/json/lib/commands/DEBUG_MEMORY.spec.ts +++ b/packages/json/lib/commands/DEBUG_MEMORY.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DEBUG_MEMORY from './DEBUG_MEMORY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.DEBUG MEMORY', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - DEBUG_MEMORY.transformArguments('key'), + parseArgs(DEBUG_MEMORY, 'key'), ['JSON.DEBUG', 'MEMORY', 'key'] ); }); it('with path', () => { assert.deepEqual( - DEBUG_MEMORY.transformArguments('key', { + parseArgs(DEBUG_MEMORY, 'key', { path: '$' }), ['JSON.DEBUG', 'MEMORY', 'key', '$'] diff --git a/packages/json/lib/commands/DEBUG_MEMORY.ts b/packages/json/lib/commands/DEBUG_MEMORY.ts index c2e730b9dc2..5c4641c3270 100644 --- a/packages/json/lib/commands/DEBUG_MEMORY.ts +++ b/packages/json/lib/commands/DEBUG_MEMORY.ts @@ -1,20 +1,19 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; export interface JsonDebugMemoryOptions { path?: RedisArgument; } export default { - FIRST_KEY_INDEX: 2, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: JsonDebugMemoryOptions) { - const args = ['JSON.DEBUG', 'MEMORY', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDebugMemoryOptions) { + parser.push('JSON.DEBUG', 'MEMORY'); + parser.pushKey(key); if (options?.path !== undefined) { - args.push(options.path); + parser.push(options.path); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/json/lib/commands/DEL.spec.ts b/packages/json/lib/commands/DEL.spec.ts index 18f6a8f2db6..a008c3b9b2b 100644 --- a/packages/json/lib/commands/DEL.spec.ts +++ b/packages/json/lib/commands/DEL.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DEL from './DEL'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.DEL', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - DEL.transformArguments('key'), + parseArgs(DEL, 'key'), ['JSON.DEL', 'key'] ); }); it('with path', () => { assert.deepEqual( - DEL.transformArguments('key', { + parseArgs(DEL, 'key', { path: '$.path' }), ['JSON.DEL', 'key', '$.path'] diff --git a/packages/json/lib/commands/DEL.ts b/packages/json/lib/commands/DEL.ts index f6952a8dc63..d4d8ed4620b 100644 --- a/packages/json/lib/commands/DEL.ts +++ b/packages/json/lib/commands/DEL.ts @@ -1,20 +1,19 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; export interface JsonDelOptions { path?: RedisArgument } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: JsonDelOptions) { - const args = ['JSON.DEL', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonDelOptions) { + parser.push('JSON.DEL'); + parser.pushKey(key); if (options?.path !== undefined) { - args.push(options.path); + parser.push(options.path); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/json/lib/commands/FORGET.spec.ts b/packages/json/lib/commands/FORGET.spec.ts index 04066ec43a9..888fff5659b 100644 --- a/packages/json/lib/commands/FORGET.spec.ts +++ b/packages/json/lib/commands/FORGET.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import FORGET from './FORGET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.FORGET', () => { describe('transformArguments', () => { it('key', () => { assert.deepEqual( - FORGET.transformArguments('key'), + parseArgs(FORGET, 'key'), ['JSON.FORGET', 'key'] ); }); it('key, path', () => { assert.deepEqual( - FORGET.transformArguments('key', { + parseArgs(FORGET, 'key', { path: '$.path' }), ['JSON.FORGET', 'key', '$.path'] diff --git a/packages/json/lib/commands/FORGET.ts b/packages/json/lib/commands/FORGET.ts index 68335ee92e9..1b5b97038e0 100644 --- a/packages/json/lib/commands/FORGET.ts +++ b/packages/json/lib/commands/FORGET.ts @@ -1,20 +1,19 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; export interface JsonForgetOptions { path?: RedisArgument; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: JsonForgetOptions) { - const args = ['JSON.FORGET', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonForgetOptions) { + parser.push('JSON.FORGET'); + parser.pushKey(key); if (options?.path !== undefined) { - args.push(options.path); + parser.push(options.path); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/json/lib/commands/GET.spec.ts b/packages/json/lib/commands/GET.spec.ts index 6d6ff14f67f..0741de316e1 100644 --- a/packages/json/lib/commands/GET.spec.ts +++ b/packages/json/lib/commands/GET.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GET from './GET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.GET', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - GET.transformArguments('key'), + parseArgs(GET, 'key'), ['JSON.GET', 'key'] ); }); @@ -14,14 +15,14 @@ describe('JSON.GET', () => { describe('with path', () => { it('string', () => { assert.deepEqual( - GET.transformArguments('key', { path: '$' }), + parseArgs(GET, 'key', { path: '$' }), ['JSON.GET', 'key', '$'] ); }); it('array', () => { assert.deepEqual( - GET.transformArguments('key', { path: ['$.1', '$.2'] }), + parseArgs(GET, 'key', { path: ['$.1', '$.2'] }), ['JSON.GET', 'key', '$.1', '$.2'] ); }); diff --git a/packages/json/lib/commands/GET.ts b/packages/json/lib/commands/GET.ts index b7bcc52e3cb..3d623483d20 100644 --- a/packages/json/lib/commands/GET.ts +++ b/packages/json/lib/commands/GET.ts @@ -1,5 +1,6 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; import { transformRedisJsonNullReply } from '.'; export interface JsonGetOptions { @@ -7,16 +8,13 @@ export interface JsonGetOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: JsonGetOptions) { - let args = ['JSON.GET', key]; - + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonGetOptions) { + parser.push('JSON.GET'); + parser.pushKey(key); if (options?.path !== undefined) { - args = pushVariadicArguments(args, options.path); + parser.pushVariadic(options.path) } - - return args; }, transformReply: transformRedisJsonNullReply } as const satisfies Command; diff --git a/packages/json/lib/commands/MERGE.spec.ts b/packages/json/lib/commands/MERGE.spec.ts index 56f5d25e7d5..30a092035c5 100644 --- a/packages/json/lib/commands/MERGE.spec.ts +++ b/packages/json/lib/commands/MERGE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MERGE from './MERGE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.MERGE', () => { it('transformArguments', () => { assert.deepEqual( - MERGE.transformArguments('key', '$', 'value'), + parseArgs(MERGE, 'key', '$', 'value'), ['JSON.MERGE', 'key', '$', '"value"'] ); }); diff --git a/packages/json/lib/commands/MERGE.ts b/packages/json/lib/commands/MERGE.ts index 90cd080a06e..25ae84c6ed9 100644 --- a/packages/json/lib/commands/MERGE.ts +++ b/packages/json/lib/commands/MERGE.ts @@ -1,16 +1,13 @@ -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, path: RedisArgument, value: RedisJSON) { - return [ - 'JSON.MERGE', - key, - path, - transformRedisJsonArgument(value) - ]; + parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, value: RedisJSON) { + parser.push('JSON.MERGE'); + parser.pushKey(key); + parser.push(path, transformRedisJsonArgument(value)); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/json/lib/commands/MGET.spec.ts b/packages/json/lib/commands/MGET.spec.ts index 1bfaecd6da9..2d8efafde71 100644 --- a/packages/json/lib/commands/MGET.spec.ts +++ b/packages/json/lib/commands/MGET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MGET from './MGET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.MGET', () => { it('transformArguments', () => { assert.deepEqual( - MGET.transformArguments(['1', '2'], '$'), + parseArgs(MGET, ['1', '2'], '$'), ['JSON.MGET', '1', '2', '$'] ); }); diff --git a/packages/json/lib/commands/MGET.ts b/packages/json/lib/commands/MGET.ts index a7aea82ac2e..79241bee7c4 100644 --- a/packages/json/lib/commands/MGET.ts +++ b/packages/json/lib/commands/MGET.ts @@ -1,15 +1,13 @@ -import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; import { transformRedisJsonNullReply } from '.'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(keys: Array, path: RedisArgument) { - return [ - 'JSON.MGET', - ...keys, - path - ]; + parseCommand(parser: CommandParser, keys: Array, path: RedisArgument) { + parser.push('JSON.MGET'); + parser.pushKeys(keys); + parser.push(path); }, transformReply(reply: UnwrapReply>) { return reply.map(json => transformRedisJsonNullReply(json)) diff --git a/packages/json/lib/commands/MSET.spec.ts b/packages/json/lib/commands/MSET.spec.ts index 360890234c8..38e8b077e81 100644 --- a/packages/json/lib/commands/MSET.spec.ts +++ b/packages/json/lib/commands/MSET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MSET from './MSET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.MSET', () => { it('transformArguments', () => { assert.deepEqual( - MSET.transformArguments([{ + parseArgs(MSET, [{ key: '1', path: '$', value: 1 diff --git a/packages/json/lib/commands/MSET.ts b/packages/json/lib/commands/MSET.ts index a081bfd543a..b6b42563c70 100644 --- a/packages/json/lib/commands/MSET.ts +++ b/packages/json/lib/commands/MSET.ts @@ -1,4 +1,5 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; export interface JsonMSetItem { @@ -8,21 +9,14 @@ export interface JsonMSetItem { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(items: Array) { - const args = new Array(1 + items.length * 3); - args[0] = 'JSON.MSET'; + parseCommand(parser: CommandParser, items: Array) { + parser.push('JSON.MSET'); - let argsIndex = 1; for (let i = 0; i < items.length; i++) { - const item = items[i]; - args[argsIndex++] = item.key; - args[argsIndex++] = item.path; - args[argsIndex++] = transformRedisJsonArgument(item.value); + parser.pushKey(items[i].key); + parser.push(items[i].path, transformRedisJsonArgument(items[i].value)); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/json/lib/commands/NUMINCRBY.spec.ts b/packages/json/lib/commands/NUMINCRBY.spec.ts index d0bffd2bd2a..b438069e80f 100644 --- a/packages/json/lib/commands/NUMINCRBY.spec.ts +++ b/packages/json/lib/commands/NUMINCRBY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import NUMINCRBY from './NUMINCRBY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.NUMINCRBY', () => { it('transformArguments', () => { assert.deepEqual( - NUMINCRBY.transformArguments('key', '$', 1), + parseArgs(NUMINCRBY, 'key', '$', 1), ['JSON.NUMINCRBY', 'key', '$', '1'] ); }); diff --git a/packages/json/lib/commands/NUMINCRBY.ts b/packages/json/lib/commands/NUMINCRBY.ts index 65cc7db68a9..8c41194a9b9 100644 --- a/packages/json/lib/commands/NUMINCRBY.ts +++ b/packages/json/lib/commands/NUMINCRBY.ts @@ -1,10 +1,12 @@ -import { RedisArgument, ArrayReply, NumberReply, DoubleReply, NullReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, DoubleReply, NullReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, path: RedisArgument, by: number) { - return ['JSON.NUMINCRBY', key, path, by.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, by: number) { + parser.push('JSON.NUMINCRBY'); + parser.pushKey(key); + parser.push(path, by.toString()); }, transformReply: { 2: (reply: UnwrapReply) => { diff --git a/packages/json/lib/commands/NUMMULTBY.spec.ts b/packages/json/lib/commands/NUMMULTBY.spec.ts index 9767c2b0979..24ee932e952 100644 --- a/packages/json/lib/commands/NUMMULTBY.spec.ts +++ b/packages/json/lib/commands/NUMMULTBY.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import NUMMULTBY from './NUMMULTBY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.NUMMULTBY', () => { it('transformArguments', () => { assert.deepEqual( - NUMMULTBY.transformArguments('key', '$', 2), + parseArgs(NUMMULTBY, 'key', '$', 2), ['JSON.NUMMULTBY', 'key', '$', '2'] ); }); diff --git a/packages/json/lib/commands/NUMMULTBY.ts b/packages/json/lib/commands/NUMMULTBY.ts index 255685a9a50..5eeb4b50e88 100644 --- a/packages/json/lib/commands/NUMMULTBY.ts +++ b/packages/json/lib/commands/NUMMULTBY.ts @@ -1,11 +1,13 @@ -import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; import NUMINCRBY from './NUMINCRBY'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, path: RedisArgument, by: number) { - return ['JSON.NUMMULTBY', key, path, by.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument, by: number) { + parser.push('JSON.NUMMULTBY'); + parser.pushKey(key); + parser.push(path, by.toString()); }, transformReply: NUMINCRBY.transformReply } as const satisfies Command; diff --git a/packages/json/lib/commands/OBJKEYS.spec.ts b/packages/json/lib/commands/OBJKEYS.spec.ts index dc984cb2ce9..0d2176248e4 100644 --- a/packages/json/lib/commands/OBJKEYS.spec.ts +++ b/packages/json/lib/commands/OBJKEYS.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import OBJKEYS from './OBJKEYS'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.OBJKEYS', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - OBJKEYS.transformArguments('key'), + parseArgs(OBJKEYS, 'key'), ['JSON.OBJKEYS', 'key'] ); }); it('with path', () => { assert.deepEqual( - OBJKEYS.transformArguments('key', { + parseArgs(OBJKEYS, 'key', { path: '$' }), ['JSON.OBJKEYS', 'key', '$'] diff --git a/packages/json/lib/commands/OBJKEYS.ts b/packages/json/lib/commands/OBJKEYS.ts index fb8ae6bb034..6d23da20534 100644 --- a/packages/json/lib/commands/OBJKEYS.ts +++ b/packages/json/lib/commands/OBJKEYS.ts @@ -1,20 +1,18 @@ -import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '@redis/client/lib/RESP/types'; export interface JsonObjKeysOptions { path?: RedisArgument; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: JsonObjKeysOptions) { - const args = ['JSON.OBJKEYS', key]; - + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjKeysOptions) { + parser.push('JSON.OBJKEYS'); + parser.pushKey(key); if (options?.path !== undefined) { - args.push(options.path); + parser.push(options.path); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply | ArrayReply | NullReply> } as const satisfies Command; diff --git a/packages/json/lib/commands/OBJLEN.spec.ts b/packages/json/lib/commands/OBJLEN.spec.ts index 8f616107fd5..a5664a4d6bc 100644 --- a/packages/json/lib/commands/OBJLEN.spec.ts +++ b/packages/json/lib/commands/OBJLEN.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import OBJLEN from './OBJLEN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.OBJLEN', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - OBJLEN.transformArguments('key'), + parseArgs(OBJLEN, 'key'), ['JSON.OBJLEN', 'key'] ); }); it('with path', () => { assert.deepEqual( - OBJLEN.transformArguments('key', { + parseArgs(OBJLEN, 'key', { path: '$' }), ['JSON.OBJLEN', 'key', '$'] diff --git a/packages/json/lib/commands/OBJLEN.ts b/packages/json/lib/commands/OBJLEN.ts index f9c45e336ad..e15a72e2165 100644 --- a/packages/json/lib/commands/OBJLEN.ts +++ b/packages/json/lib/commands/OBJLEN.ts @@ -1,20 +1,18 @@ -import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/lib/RESP/types'; export interface JsonObjLenOptions { path?: RedisArgument; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, options?: JsonObjLenOptions) { - const args = ['JSON.OBJLEN', key]; - + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonObjLenOptions) { + parser.push('JSON.OBJLEN'); + parser.pushKey(key); if (options?.path !== undefined) { - args.push(options.path); + parser.push(options.path); } - - return args; }, transformReply: undefined as unknown as () => NumberReply | ArrayReply } as const satisfies Command; diff --git a/packages/json/lib/commands/RESP.spec.ts b/packages/json/lib/commands/RESP.spec.ts index 6dfc3360326..2cb3e9e15c3 100644 --- a/packages/json/lib/commands/RESP.spec.ts +++ b/packages/json/lib/commands/RESP.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; -import { transformArguments } from './RESP'; +import RESP from './RESP'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('RESP', () => { describe('transformArguments', () => { it('without path', () => { assert.deepEqual( - transformArguments('key'), + parseArgs(RESP, 'key'), ['JSON.RESP', 'key'] ); }); it('with path', () => { assert.deepEqual( - transformArguments('key', '$'), + parseArgs(RESP, 'key', '$'), ['JSON.RESP', 'key', '$'] ); }); diff --git a/packages/json/lib/commands/RESP.ts b/packages/json/lib/commands/RESP.ts index fcf54cd3537..c9713833958 100644 --- a/packages/json/lib/commands/RESP.ts +++ b/packages/json/lib/commands/RESP.ts @@ -1,15 +1,16 @@ -export const FIRST_KEY_INDEX = 1; - -export function transformArguments(key: string, path?: string): Array { - const args = ['JSON.RESP', key]; - - if (path) { - args.push(path); - } - - return args; -} +import { CommandParser } from "@redis/client/lib/client/parser"; +import { Command, RedisArgument } from "@redis/client/lib/RESP/types"; type RESPReply = Array; -export declare function transformReply(): RESPReply; +export default { + IS_READ_ONLY: true, + parseCommand(parser: CommandParser, key: RedisArgument, path?: string) { + parser.push('JSON.RESP'); + parser.pushKey(key); + if (path !== undefined) { + parser.push(path); + } + }, + transformReply: undefined as unknown as () => RESPReply + } as const satisfies Command; \ No newline at end of file diff --git a/packages/json/lib/commands/SET.spec.ts b/packages/json/lib/commands/SET.spec.ts index 15e27073281..7bd927f08e4 100644 --- a/packages/json/lib/commands/SET.spec.ts +++ b/packages/json/lib/commands/SET.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SET from './SET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.SET', () => { describe('transformArguments', () => { it('transformArguments', () => { assert.deepEqual( - SET.transformArguments('key', '$', 'json'), + parseArgs(SET, 'key', '$', 'json'), ['JSON.SET', 'key', '$', '"json"'] ); }); it('NX', () => { assert.deepEqual( - SET.transformArguments('key', '$', 'json', { NX: true }), + parseArgs(SET, 'key', '$', 'json', { NX: true }), ['JSON.SET', 'key', '$', '"json"', 'NX'] ); }); it('XX', () => { assert.deepEqual( - SET.transformArguments('key', '$', 'json', { XX: true }), + parseArgs(SET, 'key', '$', 'json', { XX: true }), ['JSON.SET', 'key', '$', '"json"', 'XX'] ); }); diff --git a/packages/json/lib/commands/SET.ts b/packages/json/lib/commands/SET.ts index 78aea4b3545..6bd89803ded 100644 --- a/packages/json/lib/commands/SET.ts +++ b/packages/json/lib/commands/SET.ts @@ -1,4 +1,5 @@ -import { RedisArgument, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, NullReply, Command } from '@redis/client/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; export interface JsonSetOptions { @@ -14,25 +15,25 @@ export interface JsonSetOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, path: RedisArgument, json: RedisJSON, options?: JsonSetOptions ) { - const args = ['JSON.SET', key, path, transformRedisJsonArgument(json)]; + parser.push('JSON.SET'); + parser.pushKey(key); + parser.push(path, transformRedisJsonArgument(json)); if (options?.condition) { - args.push(options?.condition); + parser.push(options?.condition); } else if (options?.NX) { - args.push('NX'); + parser.push('NX'); } else if (options?.XX) { - args.push('XX'); + parser.push('XX'); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> | NullReply } as const satisfies Command; diff --git a/packages/json/lib/commands/STRAPPEND.spec.ts b/packages/json/lib/commands/STRAPPEND.spec.ts index 0d8bdb57185..ebd539130e1 100644 --- a/packages/json/lib/commands/STRAPPEND.spec.ts +++ b/packages/json/lib/commands/STRAPPEND.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import STRAPPEND from './STRAPPEND'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.STRAPPEND', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - STRAPPEND.transformArguments('key', 'append'), + parseArgs(STRAPPEND, 'key', 'append'), ['JSON.STRAPPEND', 'key', '"append"'] ); }); it('with path', () => { assert.deepEqual( - STRAPPEND.transformArguments('key', 'append', { + parseArgs(STRAPPEND, 'key', 'append', { path: '$' }), ['JSON.STRAPPEND', 'key', '$', '"append"'] diff --git a/packages/json/lib/commands/STRAPPEND.ts b/packages/json/lib/commands/STRAPPEND.ts index 12ee7cc394c..97bbebf931b 100644 --- a/packages/json/lib/commands/STRAPPEND.ts +++ b/packages/json/lib/commands/STRAPPEND.ts @@ -1,4 +1,5 @@ -import { RedisArgument, Command, NullReply, NumberReply, ArrayReply } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command, NullReply, NumberReply, ArrayReply } from '@redis/client/lib/RESP/types'; import { transformRedisJsonArgument } from '.'; export interface JsonStrAppendOptions { @@ -6,17 +7,16 @@ export interface JsonStrAppendOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, append: string, options?: JsonStrAppendOptions) { - const args = ['JSON.STRAPPEND', key]; + parseCommand(parser: CommandParser, key: RedisArgument, append: string, options?: JsonStrAppendOptions) { + parser.push('JSON.STRAPPEND'); + parser.pushKey(key); if (options?.path !== undefined) { - args.push(options.path); + parser.push(options.path); } - args.push(transformRedisJsonArgument(append)); - return args; + parser.push(transformRedisJsonArgument(append)); }, transformReply: undefined as unknown as () => NumberReply | ArrayReply } as const satisfies Command; diff --git a/packages/json/lib/commands/STRLEN.spec.ts b/packages/json/lib/commands/STRLEN.spec.ts index e058e48a635..b6881b5bd52 100644 --- a/packages/json/lib/commands/STRLEN.spec.ts +++ b/packages/json/lib/commands/STRLEN.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import STRLEN from './STRLEN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.STRLEN', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - STRLEN.transformArguments('key'), + parseArgs(STRLEN, 'key'), ['JSON.STRLEN', 'key'] ); }); it('with path', () => { assert.deepEqual( - STRLEN.transformArguments('key', { + parseArgs(STRLEN, 'key', { path: '$' }), ['JSON.STRLEN', 'key', '$'] diff --git a/packages/json/lib/commands/STRLEN.ts b/packages/json/lib/commands/STRLEN.ts index 3b514d9caba..b72f30cd6da 100644 --- a/packages/json/lib/commands/STRLEN.ts +++ b/packages/json/lib/commands/STRLEN.ts @@ -1,20 +1,19 @@ -import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/lib/RESP/types'; export interface JsonStrLenOptions { path?: RedisArgument; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, options?: JsonStrLenOptions) { - const args = ['JSON.STRLEN', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonStrLenOptions) { + parser.push('JSON.STRLEN'); + parser.pushKey(key); if (options?.path) { - args.push(options.path); + parser.push(options.path); } - - return args; }, transformReply: undefined as unknown as () => NumberReply | ArrayReply } as const satisfies Command; diff --git a/packages/json/lib/commands/TOGGLE.spec.ts b/packages/json/lib/commands/TOGGLE.spec.ts index c8a78877906..173c7708f4a 100644 --- a/packages/json/lib/commands/TOGGLE.spec.ts +++ b/packages/json/lib/commands/TOGGLE.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import TOGGLE from './TOGGLE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.TOGGLE', () => { it('transformArguments', () => { assert.deepEqual( - TOGGLE.transformArguments('key', '$'), + parseArgs(TOGGLE, 'key', '$'), ['JSON.TOGGLE', 'key', '$'] ); }); diff --git a/packages/json/lib/commands/TOGGLE.ts b/packages/json/lib/commands/TOGGLE.ts index 2a8df3eba36..2707d54b6fd 100644 --- a/packages/json/lib/commands/TOGGLE.ts +++ b/packages/json/lib/commands/TOGGLE.ts @@ -1,10 +1,12 @@ -import { RedisArgument, ArrayReply, NumberReply, NullReply, Command, } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command, } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, path: RedisArgument) { - return ['JSON.TOGGLE', key, path]; + parseCommand(parser: CommandParser, key: RedisArgument, path: RedisArgument) { + parser.push('JSON.TOGGLE'); + parser.pushKey(key); + parser.push(path); }, transformReply: undefined as unknown as () => NumberReply | NullReply | ArrayReply } as const satisfies Command; diff --git a/packages/json/lib/commands/TYPE.spec.ts b/packages/json/lib/commands/TYPE.spec.ts index 103ce59de6e..1b6ad109816 100644 --- a/packages/json/lib/commands/TYPE.spec.ts +++ b/packages/json/lib/commands/TYPE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import TYPE from './TYPE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('JSON.TYPE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - TYPE.transformArguments('key'), + parseArgs(TYPE, 'key'), ['JSON.TYPE', 'key'] ); }); it('with path', () => { assert.deepEqual( - TYPE.transformArguments('key', { + parseArgs(TYPE, 'key', { path: '$' }), ['JSON.TYPE', 'key', '$'] diff --git a/packages/json/lib/commands/TYPE.ts b/packages/json/lib/commands/TYPE.ts index c2eea9856e1..d19de31df68 100644 --- a/packages/json/lib/commands/TYPE.ts +++ b/packages/json/lib/commands/TYPE.ts @@ -1,20 +1,19 @@ -import { NullReply, BlobStringReply, ArrayReply, Command, RedisArgument, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { NullReply, BlobStringReply, ArrayReply, Command, RedisArgument, UnwrapReply } from '@redis/client/lib/RESP/types'; export interface JsonTypeOptions { path?: RedisArgument; } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, options?: JsonTypeOptions) { - const args = ['JSON.TYPE', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: JsonTypeOptions) { + parser.push('JSON.TYPE'); + parser.pushKey(key); if (options?.path) { - args.push(options.path); + parser.push(options.path); } - - return args; }, transformReply: { 2: undefined as unknown as () => NullReply | BlobStringReply | ArrayReply, @@ -24,4 +23,3 @@ export default { } }, } as const satisfies Command; - diff --git a/packages/json/lib/commands/index.ts b/packages/json/lib/commands/index.ts index 2724ff2565c..8ea44ce8049 100644 --- a/packages/json/lib/commands/index.ts +++ b/packages/json/lib/commands/index.ts @@ -1,4 +1,4 @@ -import { BlobStringReply, NullReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import { BlobStringReply, NullReply, UnwrapReply } from '@redis/client/lib/RESP/types'; import ARRAPPEND from './ARRAPPEND'; import ARRINDEX from './ARRINDEX'; import ARRINSERT from './ARRINSERT'; @@ -23,7 +23,7 @@ import STRAPPEND from './STRAPPEND'; import STRLEN from './STRLEN'; import TOGGLE from './TOGGLE'; import TYPE from './TYPE'; -import { isNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { isNullReply } from '@redis/client/lib/commands/generic-transformers'; export default { ARRAPPEND, diff --git a/packages/search/lib/commands/AGGREGATE.spec.ts b/packages/search/lib/commands/AGGREGATE.spec.ts index 50ef44f2bd6..787fbd1472f 100644 --- a/packages/search/lib/commands/AGGREGATE.spec.ts +++ b/packages/search/lib/commands/AGGREGATE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import AGGREGATE from './AGGREGATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('AGGREGATE', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*'), + parseArgs(AGGREGATE, 'index', '*'), ['FT.AGGREGATE', 'index', '*'] ); }); it('with VERBATIM', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { VERBATIM: true }), ['FT.AGGREGATE', 'index', '*', 'VERBATIM'] @@ -22,7 +23,7 @@ describe('AGGREGATE', () => { it('with ADDSCORES', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { ADDSCORES: true }), + parseArgs(AGGREGATE, 'index', '*', { ADDSCORES: true }), ['FT.AGGREGATE', 'index', '*', 'ADDSCORES'] ); }); @@ -32,7 +33,7 @@ describe('AGGREGATE', () => { describe('without alias', () => { it('string', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { LOAD: '@property' }), ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property'] @@ -41,7 +42,7 @@ describe('AGGREGATE', () => { it('{ identifier: string }', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { LOAD: { identifier: '@property' } @@ -53,7 +54,7 @@ describe('AGGREGATE', () => { it('with alias', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { LOAD: { identifier: '@property', AS: 'alias' @@ -66,7 +67,7 @@ describe('AGGREGATE', () => { it('multiple', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { LOAD: ['@1', '@2'] }), ['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2'] @@ -80,7 +81,7 @@ describe('AGGREGATE', () => { describe('without properties', () => { it('without alias', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -94,7 +95,7 @@ describe('AGGREGATE', () => { it('with alias', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -111,7 +112,7 @@ describe('AGGREGATE', () => { describe('with properties', () => { it('single', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', properties: '@property', @@ -126,7 +127,7 @@ describe('AGGREGATE', () => { it('multiple', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', properties: ['@1', '@2'], @@ -143,7 +144,7 @@ describe('AGGREGATE', () => { it('COUNT_DISTINCT', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -158,7 +159,7 @@ describe('AGGREGATE', () => { it('COUNT_DISTINCTISH', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -173,7 +174,7 @@ describe('AGGREGATE', () => { it('SUM', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -188,7 +189,7 @@ describe('AGGREGATE', () => { it('MIN', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -203,7 +204,7 @@ describe('AGGREGATE', () => { it('MAX', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -218,7 +219,7 @@ describe('AGGREGATE', () => { it('AVG', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -230,10 +231,9 @@ describe('AGGREGATE', () => { ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'AVG', '1', '@property'] ); }); - it('STDDEV', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -248,7 +248,7 @@ describe('AGGREGATE', () => { it('QUANTILE', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -264,7 +264,7 @@ describe('AGGREGATE', () => { it('TOLIST', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -280,7 +280,7 @@ describe('AGGREGATE', () => { describe('FIRST_VALUE', () => { it('simple', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -297,7 +297,7 @@ describe('AGGREGATE', () => { describe('without direction', () => { it('string', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -314,7 +314,7 @@ describe('AGGREGATE', () => { it('{ property: string }', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -333,7 +333,7 @@ describe('AGGREGATE', () => { it('with direction', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -354,7 +354,7 @@ describe('AGGREGATE', () => { it('RANDOM_SAMPLE', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'GROUPBY', REDUCE: { @@ -372,7 +372,7 @@ describe('AGGREGATE', () => { describe('SORTBY', () => { it('string', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'SORTBY', BY: '@by' @@ -384,7 +384,7 @@ describe('AGGREGATE', () => { it('Array', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'SORTBY', BY: ['@1', '@2'] @@ -396,7 +396,7 @@ describe('AGGREGATE', () => { it('with MAX', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'SORTBY', BY: '@by', @@ -410,7 +410,7 @@ describe('AGGREGATE', () => { describe('APPLY', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'APPLY', expression: '@field + 1', @@ -423,7 +423,7 @@ describe('AGGREGATE', () => { describe('LIMIT', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'LIMIT', from: 0, @@ -436,7 +436,7 @@ describe('AGGREGATE', () => { describe('FILTER', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { STEPS: [{ type: 'FILTER', expression: '@field != ""' @@ -449,7 +449,7 @@ describe('AGGREGATE', () => { it('with PARAMS', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { PARAMS: { param: 'value' } @@ -460,7 +460,7 @@ describe('AGGREGATE', () => { it('with DIALECT', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { + parseArgs(AGGREGATE, 'index', '*', { DIALECT: 1 }), ['FT.AGGREGATE', 'index', '*', 'DIALECT', '1'] @@ -469,7 +469,7 @@ describe('AGGREGATE', () => { it('with TIMEOUT', () => { assert.deepEqual( - AGGREGATE.transformArguments('index', '*', { TIMEOUT: 10 }), + parseArgs(AGGREGATE, 'index', '*', { TIMEOUT: 10 }), ['FT.AGGREGATE', 'index', '*', 'TIMEOUT', '10'] ); }); diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts index cb9652622ad..0105b082682 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -1,7 +1,8 @@ -import { ArrayReply, BlobStringReply, Command, MapReply, NumberReply, RedisArgument, ReplyUnion, TypeMapping, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { ArrayReply, BlobStringReply, Command, MapReply, NumberReply, RedisArgument, ReplyUnion, TypeMapping, UnwrapReply } from '@redis/client/lib/RESP/types'; import { RediSearchProperty } from './CREATE'; -import { FtSearchParams, pushParamsArgument } from './SEARCH'; -import { pushVariadicArgument, transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { FtSearchParams, parseParamsArgument } from './SEARCH'; +import { transformTuplesReply } from '@redis/client/lib/commands/generic-transformers'; type LoadField = RediSearchProperty | { identifier: RediSearchProperty; @@ -137,12 +138,12 @@ export interface AggregateReply { }; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: false, - transformArguments(index: RedisArgument, query: RedisArgument, options?: FtAggregateOptions) { - const args = ['FT.AGGREGATE', index, query]; + parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateOptions) { + parser.push('FT.AGGREGATE', index, query); - return pushAggregateOptions(args, options); + return parseAggregateOptions(parser, options); }, transformReply: { 2: (rawReply: AggregateRawReply, preserve?: any, typeMapping?: TypeMapping): AggregateReply => { @@ -163,17 +164,17 @@ export default { unstableResp3: true } as const satisfies Command; -export function pushAggregateOptions(args: Array, options?: FtAggregateOptions) { +export function parseAggregateOptions(parser: CommandParser , options?: FtAggregateOptions) { if (options?.VERBATIM) { - args.push('VERBATIM'); + parser.push('VERBATIM'); } if (options?.ADDSCORES) { - args.push('ADDSCORES'); + parser.push('ADDSCORES'); } if (options?.LOAD) { - const length = args.push('LOAD', ''); + const args: Array = []; if (Array.isArray(options.LOAD)) { for (const load of options.LOAD) { @@ -183,36 +184,37 @@ export function pushAggregateOptions(args: Array, options?: FtAgg pushLoadField(args, options.LOAD); } - args[length - 1] = (args.length - length).toString(); + parser.push('LOAD'); + parser.pushVariadicWithLength(args); } if (options?.TIMEOUT !== undefined) { - args.push('TIMEOUT', options.TIMEOUT.toString()); + parser.push('TIMEOUT', options.TIMEOUT.toString()); } if (options?.STEPS) { for (const step of options.STEPS) { - args.push(step.type); + parser.push(step.type); switch (step.type) { case FT_AGGREGATE_STEPS.GROUPBY: if (!step.properties) { - args.push('0'); + parser.push('0'); } else { - pushVariadicArgument(args, step.properties); + parser.pushVariadicWithLength(step.properties); } if (Array.isArray(step.REDUCE)) { for (const reducer of step.REDUCE) { - pushGroupByReducer(args, reducer); + parseGroupByReducer(parser, reducer); } } else { - pushGroupByReducer(args, step.REDUCE); + parseGroupByReducer(parser, step.REDUCE); } break; case FT_AGGREGATE_STEPS.SORTBY: - const length = args.push(''); + const args: Array = []; if (Array.isArray(step.BY)) { for (const by of step.BY) { @@ -226,32 +228,30 @@ export function pushAggregateOptions(args: Array, options?: FtAgg args.push('MAX', step.MAX.toString()); } - args[length - 1] = (args.length - length).toString(); + parser.pushVariadicWithLength(args); break; case FT_AGGREGATE_STEPS.APPLY: - args.push(step.expression, 'AS', step.AS); + parser.push(step.expression, 'AS', step.AS); break; case FT_AGGREGATE_STEPS.LIMIT: - args.push(step.from.toString(), step.size.toString()); + parser.push(step.from.toString(), step.size.toString()); break; case FT_AGGREGATE_STEPS.FILTER: - args.push(step.expression); + parser.push(step.expression); break; } } } - pushParamsArgument(args, options?.PARAMS); + parseParamsArgument(parser, options?.PARAMS); if (options?.DIALECT !== undefined) { - args.push('DIALECT', options.DIALECT.toString()); + parser.push('DIALECT', options.DIALECT.toString()); } - - return args; } function pushLoadField(args: Array, toLoad: LoadField) { @@ -266,12 +266,12 @@ function pushLoadField(args: Array, toLoad: LoadField) { } } -function pushGroupByReducer(args: Array, reducer: GroupByReducers) { - args.push('REDUCE', reducer.type); +function parseGroupByReducer(parser: CommandParser, reducer: GroupByReducers) { + parser.push('REDUCE', reducer.type); switch (reducer.type) { case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT: - args.push('0'); + parser.push('0'); break; case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT_DISTINCT: @@ -282,15 +282,16 @@ function pushGroupByReducer(args: Array, reducer: GroupByReducers case FT_AGGREGATE_GROUP_BY_REDUCERS.AVG: case FT_AGGREGATE_GROUP_BY_REDUCERS.STDDEV: case FT_AGGREGATE_GROUP_BY_REDUCERS.TOLIST: - args.push('1', reducer.property); + parser.push('1', reducer.property); break; case FT_AGGREGATE_GROUP_BY_REDUCERS.QUANTILE: - args.push('2', reducer.property, reducer.quantile.toString()); + parser.push('2', reducer.property, reducer.quantile.toString()); break; case FT_AGGREGATE_GROUP_BY_REDUCERS.FIRST_VALUE: { - const length = args.push('', reducer.property) - 1; + const args: Array = [reducer.property]; + if (reducer.BY) { args.push('BY'); if (typeof reducer.BY === 'string' || reducer.BY instanceof Buffer) { @@ -303,17 +304,17 @@ function pushGroupByReducer(args: Array, reducer: GroupByReducers } } - args[length - 1] = (args.length - length).toString(); + parser.pushVariadicWithLength(args); break; } case FT_AGGREGATE_GROUP_BY_REDUCERS.RANDOM_SAMPLE: - args.push('2', reducer.property, reducer.sampleSize.toString()); + parser.push('2', reducer.property, reducer.sampleSize.toString()); break; } if (reducer.AS) { - args.push('AS', reducer.AS); + parser.push('AS', reducer.AS); } } diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts index 9db3d945f97..57f46d1e32c 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('AGGREGATE WITHCURSOR', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - AGGREGATE_WITHCURSOR.transformArguments('index', '*'), + parseArgs(AGGREGATE_WITHCURSOR, 'index', '*'), ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR'] ); }); it('with COUNT', () => { assert.deepEqual( - AGGREGATE_WITHCURSOR.transformArguments('index', '*', { + parseArgs(AGGREGATE_WITHCURSOR, 'index', '*', { COUNT: 1 }), ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'COUNT', '1'] @@ -22,7 +23,7 @@ describe('AGGREGATE WITHCURSOR', () => { it('with MAXIDLE', () => { assert.deepEqual( - AGGREGATE_WITHCURSOR.transformArguments('index', '*', { + parseArgs(AGGREGATE_WITHCURSOR, 'index', '*', { MAXIDLE: 1 }), ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'MAXIDLE', '1'] diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts index cffb86b8b44..f9a7e75942f 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts @@ -1,4 +1,5 @@ -import { RedisArgument, Command, ReplyUnion, NumberReply } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command, ReplyUnion, NumberReply } from '@redis/client/lib/RESP/types'; import AGGREGATE, { AggregateRawReply, AggregateReply, FtAggregateOptions } from './AGGREGATE'; export interface FtAggregateWithCursorOptions extends FtAggregateOptions { @@ -17,21 +18,18 @@ export interface AggregateWithCursorReply extends AggregateReply { } export default { - FIRST_KEY_INDEX: AGGREGATE.FIRST_KEY_INDEX, IS_READ_ONLY: AGGREGATE.IS_READ_ONLY, - transformArguments(index: RedisArgument, query: RedisArgument, options?: FtAggregateWithCursorOptions) { - const args = AGGREGATE.transformArguments(index, query, options); - args.push('WITHCURSOR'); + parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtAggregateWithCursorOptions) { + AGGREGATE.parseCommand(parser, index, query, options); + parser.push('WITHCURSOR'); if (options?.COUNT !== undefined) { - args.push('COUNT', options.COUNT.toString()); + parser.push('COUNT', options.COUNT.toString()); } if(options?.MAXIDLE !== undefined) { - args.push('MAXIDLE', options.MAXIDLE.toString()); + parser.push('MAXIDLE', options.MAXIDLE.toString()); } - - return args; }, transformReply: { 2: (reply: AggregateWithCursorRawReply): AggregateWithCursorReply => { @@ -44,4 +42,3 @@ export default { }, unstableResp3: true } as const satisfies Command; - diff --git a/packages/search/lib/commands/ALIASADD.spec.ts b/packages/search/lib/commands/ALIASADD.spec.ts index 3a5d02175f9..b8332aed6a6 100644 --- a/packages/search/lib/commands/ALIASADD.spec.ts +++ b/packages/search/lib/commands/ALIASADD.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ALIASADD from './ALIASADD'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.ALIASADD', () => { it('transformArguments', () => { assert.deepEqual( - ALIASADD.transformArguments('alias', 'index'), + parseArgs(ALIASADD, 'alias', 'index'), ['FT.ALIASADD', 'alias', 'index'] ); }); diff --git a/packages/search/lib/commands/ALIASADD.ts b/packages/search/lib/commands/ALIASADD.ts index 648e1fef97e..db8eb54326e 100644 --- a/packages/search/lib/commands/ALIASADD.ts +++ b/packages/search/lib/commands/ALIASADD.ts @@ -1,10 +1,11 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(alias: RedisArgument, index: RedisArgument) { - return ['FT.ALIASADD', alias, index]; + parseCommand(parser: CommandParser, alias: RedisArgument, index: RedisArgument) { + parser.push('FT.ALIASADD', alias, index); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/search/lib/commands/ALIASDEL.spec.ts b/packages/search/lib/commands/ALIASDEL.spec.ts index 3842d01b145..19c2473f8cd 100644 --- a/packages/search/lib/commands/ALIASDEL.spec.ts +++ b/packages/search/lib/commands/ALIASDEL.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ALIASDEL from './ALIASDEL'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.ALIASDEL', () => { it('transformArguments', () => { assert.deepEqual( - ALIASDEL.transformArguments('alias'), + parseArgs(ALIASDEL, 'alias'), ['FT.ALIASDEL', 'alias'] ); }); diff --git a/packages/search/lib/commands/ALIASDEL.ts b/packages/search/lib/commands/ALIASDEL.ts index 40cc45a19de..3e4b70a1943 100644 --- a/packages/search/lib/commands/ALIASDEL.ts +++ b/packages/search/lib/commands/ALIASDEL.ts @@ -1,10 +1,11 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(alias: RedisArgument) { - return ['FT.ALIASDEL', alias]; + parseCommand(parser: CommandParser, alias: RedisArgument) { + parser.push('FT.ALIASDEL', alias); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/search/lib/commands/ALIASUPDATE.spec.ts b/packages/search/lib/commands/ALIASUPDATE.spec.ts index a0e7431af6b..f23af30229c 100644 --- a/packages/search/lib/commands/ALIASUPDATE.spec.ts +++ b/packages/search/lib/commands/ALIASUPDATE.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ALIASUPDATE from './ALIASUPDATE'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.ALIASUPDATE', () => { it('transformArguments', () => { assert.deepEqual( - ALIASUPDATE.transformArguments('alias', 'index'), + parseArgs(ALIASUPDATE, 'alias', 'index'), ['FT.ALIASUPDATE', 'alias', 'index'] ); }); diff --git a/packages/search/lib/commands/ALIASUPDATE.ts b/packages/search/lib/commands/ALIASUPDATE.ts index e2b72cfe649..46f0aa3e020 100644 --- a/packages/search/lib/commands/ALIASUPDATE.ts +++ b/packages/search/lib/commands/ALIASUPDATE.ts @@ -1,10 +1,11 @@ -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(alias: RedisArgument, index: RedisArgument) { - return ['FT.ALIASUPDATE', alias, index]; + parseCommand(parser: CommandParser, alias: RedisArgument, index: RedisArgument) { + parser.push('FT.ALIASUPDATE', alias, index); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/search/lib/commands/ALTER.spec.ts b/packages/search/lib/commands/ALTER.spec.ts index 6cac0be40c8..c34f7e045d5 100644 --- a/packages/search/lib/commands/ALTER.spec.ts +++ b/packages/search/lib/commands/ALTER.spec.ts @@ -2,12 +2,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ALTER from './ALTER'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.ALTER', () => { describe('transformArguments', () => { it('with NOINDEX', () => { assert.deepEqual( - ALTER.transformArguments('index', { + parseArgs(ALTER, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, NOINDEX: true, diff --git a/packages/search/lib/commands/ALTER.ts b/packages/search/lib/commands/ALTER.ts index d5587b2397c..4cde32e6ad4 100644 --- a/packages/search/lib/commands/ALTER.ts +++ b/packages/search/lib/commands/ALTER.ts @@ -1,13 +1,13 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RediSearchSchema, pushSchema } from './CREATE'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { RediSearchSchema, parseSchema } from './CREATE'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument, schema: RediSearchSchema) { - const args = ['FT.ALTER', index, 'SCHEMA', 'ADD']; - pushSchema(args, schema); - return args; + parseCommand(parser: CommandParser, index: RedisArgument, schema: RediSearchSchema) { + parser.push('FT.ALTER', index, 'SCHEMA', 'ADD'); + parseSchema(parser, schema); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/search/lib/commands/CONFIG_GET.spec.ts b/packages/search/lib/commands/CONFIG_GET.spec.ts index 7ef2a3536b9..598a2a9ac41 100644 --- a/packages/search/lib/commands/CONFIG_GET.spec.ts +++ b/packages/search/lib/commands/CONFIG_GET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CONFIG_GET from './CONFIG_GET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.CONFIG GET', () => { it('transformArguments', () => { assert.deepEqual( - CONFIG_GET.transformArguments('TIMEOUT'), + parseArgs(CONFIG_GET, 'TIMEOUT'), ['FT.CONFIG', 'GET', 'TIMEOUT'] ); }); diff --git a/packages/search/lib/commands/CONFIG_GET.ts b/packages/search/lib/commands/CONFIG_GET.ts index f96461e8694..9c0be73e2e3 100644 --- a/packages/search/lib/commands/CONFIG_GET.ts +++ b/packages/search/lib/commands/CONFIG_GET.ts @@ -1,10 +1,11 @@ -import { ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(option: string) { - return ['FT.CONFIG', 'GET', option]; + parseCommand(parser: CommandParser, option: string) { + parser.push('FT.CONFIG', 'GET', option); }, transformReply(reply: UnwrapReply>>) { const transformedReply: Record = Object.create(null); diff --git a/packages/search/lib/commands/CONFIG_SET.spec.ts b/packages/search/lib/commands/CONFIG_SET.spec.ts index 3b20f2eac5d..71a4e69f26f 100644 --- a/packages/search/lib/commands/CONFIG_SET.spec.ts +++ b/packages/search/lib/commands/CONFIG_SET.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CONFIG_SET from './CONFIG_SET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.CONFIG SET', () => { it('transformArguments', () => { assert.deepEqual( - CONFIG_SET.transformArguments('TIMEOUT', '500'), + parseArgs(CONFIG_SET, 'TIMEOUT', '500'), ['FT.CONFIG', 'SET', 'TIMEOUT', '500'] ); }); diff --git a/packages/search/lib/commands/CONFIG_SET.ts b/packages/search/lib/commands/CONFIG_SET.ts index ac001bf68a6..ae57dd7d8f6 100644 --- a/packages/search/lib/commands/CONFIG_SET.ts +++ b/packages/search/lib/commands/CONFIG_SET.ts @@ -1,14 +1,15 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; // using `string & {}` to avoid TS widening the type to `string` // TODO type FtConfigProperties = 'a' | 'b' | (string & {}) | Buffer; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(property: FtConfigProperties, value: RedisArgument) { - return ['FT.CONFIG', 'SET', property, value]; + parseCommand(parser: CommandParser, property: FtConfigProperties, value: RedisArgument) { + parser.push('FT.CONFIG', 'SET', property, value); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/search/lib/commands/CREATE.spec.ts b/packages/search/lib/commands/CREATE.spec.ts index bc48691bd58..58888fb7ce4 100644 --- a/packages/search/lib/commands/CREATE.spec.ts +++ b/packages/search/lib/commands/CREATE.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CREATE, { SCHEMA_FIELD_TYPE, SCHEMA_TEXT_FIELD_PHONETIC, SCHEMA_VECTOR_FIELD_ALGORITHM, REDISEARCH_LANGUAGE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.CREATE', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - CREATE.transformArguments('index', {}), + parseArgs(CREATE, 'index', {}), ['FT.CREATE', 'index', 'SCHEMA'] ); }); @@ -15,7 +16,7 @@ describe('FT.CREATE', () => { describe('TEXT', () => { it('without options', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: SCHEMA_FIELD_TYPE.TEXT }), ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT'] @@ -24,7 +25,7 @@ describe('FT.CREATE', () => { it('with NOSTEM', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, NOSTEM: true @@ -36,7 +37,7 @@ describe('FT.CREATE', () => { it('with WEIGHT', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, WEIGHT: 1 @@ -48,7 +49,7 @@ describe('FT.CREATE', () => { it('with PHONETIC', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, PHONETIC: SCHEMA_TEXT_FIELD_PHONETIC.DM_EN @@ -60,7 +61,7 @@ describe('FT.CREATE', () => { it('with WITHSUFFIXTRIE', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, WITHSUFFIXTRIE: true @@ -73,7 +74,7 @@ describe('FT.CREATE', () => { it('NUMERIC', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: SCHEMA_FIELD_TYPE.NUMERIC }), ['FT.CREATE', 'index', 'SCHEMA', 'field', 'NUMERIC'] @@ -82,7 +83,7 @@ describe('FT.CREATE', () => { it('GEO', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: SCHEMA_FIELD_TYPE.GEO }), ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEO'] @@ -93,7 +94,7 @@ describe('FT.CREATE', () => { describe('without options', () => { it('SCHEMA_FIELD_TYPE.TAG', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: SCHEMA_FIELD_TYPE.TAG }), ['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG'] @@ -102,7 +103,7 @@ describe('FT.CREATE', () => { it('{ type: SCHEMA_FIELD_TYPE.TAG }', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TAG } @@ -114,7 +115,7 @@ describe('FT.CREATE', () => { it('with SEPARATOR', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TAG, SEPARATOR: 'separator' @@ -126,7 +127,7 @@ describe('FT.CREATE', () => { it('with CASESENSITIVE', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TAG, CASESENSITIVE: true @@ -138,7 +139,7 @@ describe('FT.CREATE', () => { it('with WITHSUFFIXTRIE', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TAG, WITHSUFFIXTRIE: true @@ -150,7 +151,7 @@ describe('FT.CREATE', () => { it('with INDEXEMPTY', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TAG, INDEXEMPTY: true @@ -164,7 +165,7 @@ describe('FT.CREATE', () => { describe('VECTOR', () => { it('Flat algorithm', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.VECTOR, ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT, @@ -185,7 +186,7 @@ describe('FT.CREATE', () => { it('HNSW algorithm', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.VECTOR, ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW, @@ -211,7 +212,7 @@ describe('FT.CREATE', () => { describe('without options', () => { it('SCHEMA_FIELD_TYPE.GEOSHAPE', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: SCHEMA_FIELD_TYPE.GEOSHAPE }), ['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE'] @@ -220,7 +221,7 @@ describe('FT.CREATE', () => { it('{ type: SCHEMA_FIELD_TYPE.GEOSHAPE }', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.GEOSHAPE } @@ -232,7 +233,7 @@ describe('FT.CREATE', () => { it('with COORD_SYSTEM', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.GEOSHAPE, COORD_SYSTEM: 'SPHERICAL' @@ -245,7 +246,7 @@ describe('FT.CREATE', () => { it('with AS', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, AS: 'as' @@ -258,7 +259,7 @@ describe('FT.CREATE', () => { describe('with SORTABLE', () => { it('true', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, SORTABLE: true @@ -270,7 +271,7 @@ describe('FT.CREATE', () => { it('UNF', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, SORTABLE: 'UNF' @@ -283,7 +284,7 @@ describe('FT.CREATE', () => { it('with NOINDEX', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, NOINDEX: true @@ -295,7 +296,7 @@ describe('FT.CREATE', () => { it('with INDEXMISSING', () => { assert.deepEqual( - CREATE.transformArguments('index', { + parseArgs(CREATE, 'index', { field: { type: SCHEMA_FIELD_TYPE.TEXT, INDEXMISSING: true @@ -308,7 +309,7 @@ describe('FT.CREATE', () => { it('with ON', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { ON: 'HASH' }), ['FT.CREATE', 'index', 'ON', 'HASH', 'SCHEMA'] @@ -318,7 +319,7 @@ describe('FT.CREATE', () => { describe('with PREFIX', () => { it('string', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { PREFIX: 'prefix' }), ['FT.CREATE', 'index', 'PREFIX', '1', 'prefix', 'SCHEMA'] @@ -327,7 +328,7 @@ describe('FT.CREATE', () => { it('Array', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { PREFIX: ['1', '2'] }), ['FT.CREATE', 'index', 'PREFIX', '2', '1', '2', 'SCHEMA'] @@ -337,7 +338,7 @@ describe('FT.CREATE', () => { it('with FILTER', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { FILTER: '@field != ""' }), ['FT.CREATE', 'index', 'FILTER', '@field != ""', 'SCHEMA'] @@ -346,7 +347,7 @@ describe('FT.CREATE', () => { it('with LANGUAGE', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { LANGUAGE: REDISEARCH_LANGUAGE.ARABIC }), ['FT.CREATE', 'index', 'LANGUAGE', REDISEARCH_LANGUAGE.ARABIC, 'SCHEMA'] @@ -355,7 +356,7 @@ describe('FT.CREATE', () => { it('with LANGUAGE_FIELD', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { LANGUAGE_FIELD: '@field' }), ['FT.CREATE', 'index', 'LANGUAGE_FIELD', '@field', 'SCHEMA'] @@ -364,7 +365,7 @@ describe('FT.CREATE', () => { it('with SCORE', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { SCORE: 1 }), ['FT.CREATE', 'index', 'SCORE', '1', 'SCHEMA'] @@ -373,7 +374,7 @@ describe('FT.CREATE', () => { it('with SCORE_FIELD', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { SCORE_FIELD: '@field' }), ['FT.CREATE', 'index', 'SCORE_FIELD', '@field', 'SCHEMA'] @@ -382,7 +383,7 @@ describe('FT.CREATE', () => { it('with MAXTEXTFIELDS', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { MAXTEXTFIELDS: true }), ['FT.CREATE', 'index', 'MAXTEXTFIELDS', 'SCHEMA'] @@ -391,7 +392,7 @@ describe('FT.CREATE', () => { it('with TEMPORARY', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { TEMPORARY: 1 }), ['FT.CREATE', 'index', 'TEMPORARY', '1', 'SCHEMA'] @@ -400,7 +401,7 @@ describe('FT.CREATE', () => { it('with NOOFFSETS', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { NOOFFSETS: true }), ['FT.CREATE', 'index', 'NOOFFSETS', 'SCHEMA'] @@ -409,7 +410,7 @@ describe('FT.CREATE', () => { it('with NOHL', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { NOHL: true }), ['FT.CREATE', 'index', 'NOHL', 'SCHEMA'] @@ -418,7 +419,7 @@ describe('FT.CREATE', () => { it('with NOFIELDS', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { NOFIELDS: true }), ['FT.CREATE', 'index', 'NOFIELDS', 'SCHEMA'] @@ -427,7 +428,7 @@ describe('FT.CREATE', () => { it('with NOFREQS', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { NOFREQS: true }), ['FT.CREATE', 'index', 'NOFREQS', 'SCHEMA'] @@ -436,7 +437,7 @@ describe('FT.CREATE', () => { it('with SKIPINITIALSCAN', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { SKIPINITIALSCAN: true }), ['FT.CREATE', 'index', 'SKIPINITIALSCAN', 'SCHEMA'] @@ -446,7 +447,7 @@ describe('FT.CREATE', () => { describe('with STOPWORDS', () => { it('string', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { STOPWORDS: 'stopword' }), ['FT.CREATE', 'index', 'STOPWORDS', '1', 'stopword', 'SCHEMA'] @@ -455,7 +456,7 @@ describe('FT.CREATE', () => { it('Array', () => { assert.deepEqual( - CREATE.transformArguments('index', {}, { + parseArgs(CREATE, 'index', {}, { STOPWORDS: ['1', '2'] }), ['FT.CREATE', 'index', 'STOPWORDS', '2', '1', '2', 'SCHEMA'] diff --git a/packages/search/lib/commands/CREATE.ts b/packages/search/lib/commands/CREATE.ts index 2951e56f090..d0096282f37 100644 --- a/packages/search/lib/commands/CREATE.ts +++ b/packages/search/lib/commands/CREATE.ts @@ -1,5 +1,6 @@ -import { RedisArgument, SimpleStringReply, Command, CommandArguments } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument, pushOptionalVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export const SCHEMA_FIELD_TYPE = { TEXT: 'TEXT', @@ -102,93 +103,93 @@ export interface RediSearchSchema { ); } -function pushCommonSchemaFieldOptions(args: CommandArguments, fieldOptions: SchemaCommonField) { +function parseCommonSchemaFieldOptions(parser: CommandParser, fieldOptions: SchemaCommonField) { if (fieldOptions.SORTABLE) { - args.push('SORTABLE'); + parser.push('SORTABLE'); if (fieldOptions.SORTABLE === 'UNF') { - args.push('UNF'); + parser.push('UNF'); } } if (fieldOptions.NOINDEX) { - args.push('NOINDEX'); + parser.push('NOINDEX'); } } -export function pushSchema(args: CommandArguments, schema: RediSearchSchema) { +export function parseSchema(parser: CommandParser, schema: RediSearchSchema) { for (const [field, fieldOptions] of Object.entries(schema)) { - args.push(field); + parser.push(field); if (typeof fieldOptions === 'string') { - args.push(fieldOptions); + parser.push(fieldOptions); continue; } if (fieldOptions.AS) { - args.push('AS', fieldOptions.AS); + parser.push('AS', fieldOptions.AS); } - args.push(fieldOptions.type); + parser.push(fieldOptions.type); if (fieldOptions.INDEXMISSING) { - args.push('INDEXMISSING'); + parser.push('INDEXMISSING'); } switch (fieldOptions.type) { case SCHEMA_FIELD_TYPE.TEXT: if (fieldOptions.NOSTEM) { - args.push('NOSTEM'); + parser.push('NOSTEM'); } if (fieldOptions.WEIGHT) { - args.push('WEIGHT', fieldOptions.WEIGHT.toString()); + parser.push('WEIGHT', fieldOptions.WEIGHT.toString()); } if (fieldOptions.PHONETIC) { - args.push('PHONETIC', fieldOptions.PHONETIC); + parser.push('PHONETIC', fieldOptions.PHONETIC); } if (fieldOptions.WITHSUFFIXTRIE) { - args.push('WITHSUFFIXTRIE'); + parser.push('WITHSUFFIXTRIE'); } if (fieldOptions.INDEXEMPTY) { - args.push('INDEXEMPTY'); + parser.push('INDEXEMPTY'); } - pushCommonSchemaFieldOptions(args, fieldOptions) + parseCommonSchemaFieldOptions(parser, fieldOptions) break; case SCHEMA_FIELD_TYPE.NUMERIC: case SCHEMA_FIELD_TYPE.GEO: - pushCommonSchemaFieldOptions(args, fieldOptions) + parseCommonSchemaFieldOptions(parser, fieldOptions) break; case SCHEMA_FIELD_TYPE.TAG: if (fieldOptions.SEPARATOR) { - args.push('SEPARATOR', fieldOptions.SEPARATOR); + parser.push('SEPARATOR', fieldOptions.SEPARATOR); } if (fieldOptions.CASESENSITIVE) { - args.push('CASESENSITIVE'); + parser.push('CASESENSITIVE'); } if (fieldOptions.WITHSUFFIXTRIE) { - args.push('WITHSUFFIXTRIE'); + parser.push('WITHSUFFIXTRIE'); } if (fieldOptions.INDEXEMPTY) { - args.push('INDEXEMPTY'); + parser.push('INDEXEMPTY'); } - pushCommonSchemaFieldOptions(args, fieldOptions) + parseCommonSchemaFieldOptions(parser, fieldOptions) break; case SCHEMA_FIELD_TYPE.VECTOR: - args.push(fieldOptions.ALGORITHM); + parser.push(fieldOptions.ALGORITHM); - const lengthIndex = args.push('') - 1; + const args: Array = []; args.push( 'TYPE', fieldOptions.TYPE, @@ -200,7 +201,7 @@ export function pushSchema(args: CommandArguments, schema: RediSearchSchema) { args.push('INITIAL_CAP', fieldOptions.INITIAL_CAP.toString()); } - switch (fieldOptions.ALGORITHM) { + switch (fieldOptions.ALGORITHM) { case SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT: if (fieldOptions.BLOCK_SIZE) { args.push('BLOCK_SIZE', fieldOptions.BLOCK_SIZE.toString()); @@ -223,13 +224,13 @@ export function pushSchema(args: CommandArguments, schema: RediSearchSchema) { break; } - args[lengthIndex] = (args.length - lengthIndex - 1).toString(); + parser.pushVariadicWithLength(args); break; case SCHEMA_FIELD_TYPE.GEOSHAPE: if (fieldOptions.COORD_SYSTEM !== undefined) { - args.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM); + parser.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM); } break; @@ -289,74 +290,72 @@ export interface CreateOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument, schema: RediSearchSchema, options?: CreateOptions) { - const args = ['FT.CREATE', index]; + parseCommand(parser: CommandParser, index: RedisArgument, schema: RediSearchSchema, options?: CreateOptions) { + parser.push('FT.CREATE', index); if (options?.ON) { - args.push('ON', options.ON); + parser.push('ON', options.ON); } - pushOptionalVariadicArgument(args, 'PREFIX', options?.PREFIX); + parseOptionalVariadicArgument(parser, 'PREFIX', options?.PREFIX); if (options?.FILTER) { - args.push('FILTER', options.FILTER); + parser.push('FILTER', options.FILTER); } if (options?.LANGUAGE) { - args.push('LANGUAGE', options.LANGUAGE); + parser.push('LANGUAGE', options.LANGUAGE); } if (options?.LANGUAGE_FIELD) { - args.push('LANGUAGE_FIELD', options.LANGUAGE_FIELD); + parser.push('LANGUAGE_FIELD', options.LANGUAGE_FIELD); } if (options?.SCORE) { - args.push('SCORE', options.SCORE.toString()); + parser.push('SCORE', options.SCORE.toString()); } if (options?.SCORE_FIELD) { - args.push('SCORE_FIELD', options.SCORE_FIELD); + parser.push('SCORE_FIELD', options.SCORE_FIELD); } // if (options?.PAYLOAD_FIELD) { - // args.push('PAYLOAD_FIELD', options.PAYLOAD_FIELD); + // parser.push('PAYLOAD_FIELD', options.PAYLOAD_FIELD); // } if (options?.MAXTEXTFIELDS) { - args.push('MAXTEXTFIELDS'); + parser.push('MAXTEXTFIELDS'); } if (options?.TEMPORARY) { - args.push('TEMPORARY', options.TEMPORARY.toString()); + parser.push('TEMPORARY', options.TEMPORARY.toString()); } if (options?.NOOFFSETS) { - args.push('NOOFFSETS'); + parser.push('NOOFFSETS'); } if (options?.NOHL) { - args.push('NOHL'); + parser.push('NOHL'); } if (options?.NOFIELDS) { - args.push('NOFIELDS'); + parser.push('NOFIELDS'); } if (options?.NOFREQS) { - args.push('NOFREQS'); + parser.push('NOFREQS'); } if (options?.SKIPINITIALSCAN) { - args.push('SKIPINITIALSCAN'); + parser.push('SKIPINITIALSCAN'); } - pushOptionalVariadicArgument(args, 'STOPWORDS', options?.STOPWORDS); - args.push('SCHEMA'); - pushSchema(args, schema); - - return args; + parseOptionalVariadicArgument(parser, 'STOPWORDS', options?.STOPWORDS); + parser.push('SCHEMA'); + parseSchema(parser, schema); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/search/lib/commands/CURSOR_DEL.spec.ts b/packages/search/lib/commands/CURSOR_DEL.spec.ts index 8e9a7cf9aec..230a5fd0feb 100644 --- a/packages/search/lib/commands/CURSOR_DEL.spec.ts +++ b/packages/search/lib/commands/CURSOR_DEL.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CURSOR_DEL from './CURSOR_DEL'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.CURSOR DEL', () => { it('transformArguments', () => { assert.deepEqual( - CURSOR_DEL.transformArguments('index', 0), + parseArgs(CURSOR_DEL, 'index', 0), ['FT.CURSOR', 'DEL', 'index', '0'] ); }); diff --git a/packages/search/lib/commands/CURSOR_DEL.ts b/packages/search/lib/commands/CURSOR_DEL.ts index afccd695ff3..1ea4b46c8bd 100644 --- a/packages/search/lib/commands/CURSOR_DEL.ts +++ b/packages/search/lib/commands/CURSOR_DEL.ts @@ -1,10 +1,11 @@ -import { SimpleStringReply, Command, RedisArgument, NumberReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument, NumberReply, UnwrapReply } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument, cursorId: UnwrapReply) { - return ['FT.CURSOR', 'DEL', index, cursorId.toString()]; + parseCommand(parser: CommandParser, index: RedisArgument, cursorId: UnwrapReply) { + parser.push('FT.CURSOR', 'DEL', index, cursorId.toString()); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/search/lib/commands/CURSOR_READ.spec.ts b/packages/search/lib/commands/CURSOR_READ.spec.ts index 5999d4a7c18..42dca0c5756 100644 --- a/packages/search/lib/commands/CURSOR_READ.spec.ts +++ b/packages/search/lib/commands/CURSOR_READ.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CURSOR_READ from './CURSOR_READ'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.CURSOR READ', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - CURSOR_READ.transformArguments('index', 0), + parseArgs(CURSOR_READ, 'index', '0'), ['FT.CURSOR', 'READ', 'index', '0'] ); }); it('with COUNT', () => { assert.deepEqual( - CURSOR_READ.transformArguments('index', 0, { + parseArgs(CURSOR_READ, 'index', '0', { COUNT: 1 }), ['FT.CURSOR', 'READ', 'index', '0', 'COUNT', '1'] diff --git a/packages/search/lib/commands/CURSOR_READ.ts b/packages/search/lib/commands/CURSOR_READ.ts index d08b22ba90d..d23a0f0bd15 100644 --- a/packages/search/lib/commands/CURSOR_READ.ts +++ b/packages/search/lib/commands/CURSOR_READ.ts @@ -1,4 +1,5 @@ -import { RedisArgument, Command, UnwrapReply, NumberReply } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command, NumberReply, UnwrapReply } from '@redis/client/lib/RESP/types'; import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR'; export interface FtCursorReadOptions { @@ -6,16 +7,14 @@ export interface FtCursorReadOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument, cursor: UnwrapReply, options?: FtCursorReadOptions) { - const args = ['FT.CURSOR', 'READ', index, cursor.toString()]; + parseCommand(parser: CommandParser, index: RedisArgument, cursor: UnwrapReply, options?: FtCursorReadOptions) { + parser.push('FT.CURSOR', 'READ', index, cursor.toString()); if (options?.COUNT !== undefined) { - args.push('COUNT', options.COUNT.toString()); + parser.push('COUNT', options.COUNT.toString()); } - - return args; }, transformReply: AGGREGATE_WITHCURSOR.transformReply, unstableResp3: true diff --git a/packages/search/lib/commands/DICTADD.spec.ts b/packages/search/lib/commands/DICTADD.spec.ts index c18502ea4d0..4707db02dcf 100644 --- a/packages/search/lib/commands/DICTADD.spec.ts +++ b/packages/search/lib/commands/DICTADD.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DICTADD from './DICTADD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.DICTADD', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - DICTADD.transformArguments('dictionary', 'term'), + parseArgs(DICTADD, 'dictionary', 'term'), ['FT.DICTADD', 'dictionary', 'term'] ); }); it('Array', () => { assert.deepEqual( - DICTADD.transformArguments('dictionary', ['1', '2']), + parseArgs(DICTADD, 'dictionary', ['1', '2']), ['FT.DICTADD', 'dictionary', '1', '2'] ); }); diff --git a/packages/search/lib/commands/DICTADD.ts b/packages/search/lib/commands/DICTADD.ts index f633d58b1f3..67f94a82c13 100644 --- a/packages/search/lib/commands/DICTADD.ts +++ b/packages/search/lib/commands/DICTADD.ts @@ -1,11 +1,13 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { pushVariadicArguments, RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(dictionary: RedisArgument, term: RedisVariadicArgument) { - return pushVariadicArguments(['FT.DICTADD', dictionary], term); + parseCommand(parser: CommandParser, dictionary: RedisArgument, term: RedisVariadicArgument) { + parser.push('FT.DICTADD', dictionary); + parser.pushVariadic(term); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/search/lib/commands/DICTDEL.spec.ts b/packages/search/lib/commands/DICTDEL.spec.ts index a7ca1b35cd3..a9f997bdf38 100644 --- a/packages/search/lib/commands/DICTDEL.spec.ts +++ b/packages/search/lib/commands/DICTDEL.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DICTDEL from './DICTDEL'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.DICTDEL', () => { describe('transformArguments', () => { it('string', () => { assert.deepEqual( - DICTDEL.transformArguments('dictionary', 'term'), + parseArgs(DICTDEL, 'dictionary', 'term'), ['FT.DICTDEL', 'dictionary', 'term'] ); }); it('Array', () => { assert.deepEqual( - DICTDEL.transformArguments('dictionary', ['1', '2']), + parseArgs(DICTDEL, 'dictionary', ['1', '2']), ['FT.DICTDEL', 'dictionary', '1', '2'] ); }); diff --git a/packages/search/lib/commands/DICTDEL.ts b/packages/search/lib/commands/DICTDEL.ts index 087211751ee..9b0bda3a7a6 100644 --- a/packages/search/lib/commands/DICTDEL.ts +++ b/packages/search/lib/commands/DICTDEL.ts @@ -1,11 +1,13 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { pushVariadicArguments, RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(dictionary: RedisArgument, term: RedisVariadicArgument) { - return pushVariadicArguments(['FT.DICTDEL', dictionary], term); + parseCommand(parser: CommandParser, dictionary: RedisArgument, term: RedisVariadicArgument) { + parser.push('FT.DICTDEL', dictionary); + parser.pushVariadic(term); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/search/lib/commands/DICTDUMP.spec.ts b/packages/search/lib/commands/DICTDUMP.spec.ts index fe8e9441189..1a3faa9dc9d 100644 --- a/packages/search/lib/commands/DICTDUMP.spec.ts +++ b/packages/search/lib/commands/DICTDUMP.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DICTDUMP from './DICTDUMP'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.DICTDUMP', () => { it('transformArguments', () => { assert.deepEqual( - DICTDUMP.transformArguments('dictionary'), + parseArgs(DICTDUMP, 'dictionary'), ['FT.DICTDUMP', 'dictionary'] ); }); diff --git a/packages/search/lib/commands/DICTDUMP.ts b/packages/search/lib/commands/DICTDUMP.ts index f542403cc57..00dd5aba4eb 100644 --- a/packages/search/lib/commands/DICTDUMP.ts +++ b/packages/search/lib/commands/DICTDUMP.ts @@ -1,10 +1,11 @@ -import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(dictionary: RedisArgument) { - return ['FT.DICTDUMP', dictionary]; + parseCommand(parser: CommandParser, dictionary: RedisArgument) { + parser.push('FT.DICTDUMP', dictionary); }, transformReply: { 2: undefined as unknown as () => ArrayReply, diff --git a/packages/search/lib/commands/DROPINDEX.spec.ts b/packages/search/lib/commands/DROPINDEX.spec.ts index 5fcbaca08ce..f1f0b0efddb 100644 --- a/packages/search/lib/commands/DROPINDEX.spec.ts +++ b/packages/search/lib/commands/DROPINDEX.spec.ts @@ -2,19 +2,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DROPINDEX from './DROPINDEX'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.DROPINDEX', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - DROPINDEX.transformArguments('index'), + parseArgs(DROPINDEX, 'index'), ['FT.DROPINDEX', 'index'] ); }); it('with DD', () => { assert.deepEqual( - DROPINDEX.transformArguments('index', { DD: true }), + parseArgs(DROPINDEX, 'index', { DD: true }), ['FT.DROPINDEX', 'index', 'DD'] ); }); diff --git a/packages/search/lib/commands/DROPINDEX.ts b/packages/search/lib/commands/DROPINDEX.ts index 64fe9711e7f..e7be806ac14 100644 --- a/packages/search/lib/commands/DROPINDEX.ts +++ b/packages/search/lib/commands/DROPINDEX.ts @@ -1,20 +1,19 @@ -import { RedisArgument, SimpleStringReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; export interface FtDropIndexOptions { DD?: true; } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument, options?: FtDropIndexOptions) { - const args = ['FT.DROPINDEX', index]; + parseCommand(parser: CommandParser, index: RedisArgument, options?: FtDropIndexOptions) { + parser.push('FT.DROPINDEX', index); if (options?.DD) { - args.push('DD'); + parser.push('DD'); } - - return args; }, transformReply: { 2: undefined as unknown as () => SimpleStringReply<'OK'>, diff --git a/packages/search/lib/commands/EXPLAIN.spec.ts b/packages/search/lib/commands/EXPLAIN.spec.ts index e8b3555957f..ddc551fbd71 100644 --- a/packages/search/lib/commands/EXPLAIN.spec.ts +++ b/packages/search/lib/commands/EXPLAIN.spec.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'node:assert'; import EXPLAIN from './EXPLAIN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; import testUtils, { GLOBAL } from '../test-utils'; import { SCHEMA_FIELD_TYPE } from './CREATE'; @@ -7,14 +8,14 @@ describe('EXPLAIN', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( - EXPLAIN.transformArguments('index', '*'), + parseArgs(EXPLAIN, 'index', '*'), ['FT.EXPLAIN', 'index', '*'] ); }); it('with PARAMS', () => { assert.deepEqual( - EXPLAIN.transformArguments('index', '*', { + parseArgs(EXPLAIN, 'index', '*', { PARAMS: { param: 'value' } @@ -25,7 +26,7 @@ describe('EXPLAIN', () => { it('with DIALECT', () => { assert.deepEqual( - EXPLAIN.transformArguments('index', '*', { + parseArgs(EXPLAIN, 'index', '*', { DIALECT: 1 }), ['FT.EXPLAIN', 'index', '*', 'DIALECT', '1'] diff --git a/packages/search/lib/commands/EXPLAIN.ts b/packages/search/lib/commands/EXPLAIN.ts index 0ad84feb68d..deb75229b5d 100644 --- a/packages/search/lib/commands/EXPLAIN.ts +++ b/packages/search/lib/commands/EXPLAIN.ts @@ -1,5 +1,6 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { FtSearchParams, pushParamsArgument } from './SEARCH'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { FtSearchParams, parseParamsArgument } from './SEARCH'; export interface FtExplainOptions { PARAMS?: FtSearchParams; @@ -7,22 +8,21 @@ export interface FtExplainOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtExplainOptions ) { - const args = ['FT.EXPLAIN', index, query]; + parser.push('FT.EXPLAIN', index, query); - pushParamsArgument(args, options?.PARAMS); + parseParamsArgument(parser, options?.PARAMS); if (options?.DIALECT) { - args.push('DIALECT', options.DIALECT.toString()); + parser.push('DIALECT', options.DIALECT.toString()); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply } as const satisfies Command; diff --git a/packages/search/lib/commands/EXPLAINCLI.spec.ts b/packages/search/lib/commands/EXPLAINCLI.spec.ts index 3bffcf5fe5b..cf46a1740c5 100644 --- a/packages/search/lib/commands/EXPLAINCLI.spec.ts +++ b/packages/search/lib/commands/EXPLAINCLI.spec.ts @@ -1,10 +1,11 @@ import { strict as assert } from 'node:assert'; import EXPLAINCLI from './EXPLAINCLI'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('EXPLAINCLI', () => { it('transformArguments', () => { assert.deepEqual( - EXPLAINCLI.transformArguments('index', '*'), + parseArgs(EXPLAINCLI, 'index', '*'), ['FT.EXPLAINCLI', 'index', '*'] ); }); diff --git a/packages/search/lib/commands/EXPLAINCLI.ts b/packages/search/lib/commands/EXPLAINCLI.ts index e16866991b9..7a4ae3a4b25 100644 --- a/packages/search/lib/commands/EXPLAINCLI.ts +++ b/packages/search/lib/commands/EXPLAINCLI.ts @@ -1,10 +1,11 @@ -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument, query: RedisArgument) { - return ['FT.EXPLAINCLI', index, query]; + parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument) { + parser.push('FT.EXPLAINCLI', index, query); }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/search/lib/commands/INFO.spec.ts b/packages/search/lib/commands/INFO.spec.ts index e7c7c897a84..cbb4ea91677 100644 --- a/packages/search/lib/commands/INFO.spec.ts +++ b/packages/search/lib/commands/INFO.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import INFO, { InfoReply } from './INFO'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('INFO', () => { it('transformArguments', () => { assert.deepEqual( - INFO.transformArguments('index'), + parseArgs(INFO, 'index'), ['FT.INFO', 'index'] ); }); diff --git a/packages/search/lib/commands/INFO.ts b/packages/search/lib/commands/INFO.ts index 52b87769cef..6792645fe3e 100644 --- a/packages/search/lib/commands/INFO.ts +++ b/packages/search/lib/commands/INFO.ts @@ -1,13 +1,14 @@ +import { CommandParser } from '@redis/client/lib/client/parser'; import { RedisArgument } from "@redis/client"; -import { ArrayReply, BlobStringReply, Command, DoubleReply, MapReply, NullReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/dist/lib/RESP/types"; -import { createTransformTuplesReplyFunc, transformDoubleReply } from "@redis/client/dist/lib/commands/generic-transformers"; +import { ArrayReply, BlobStringReply, Command, DoubleReply, MapReply, NullReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/lib/RESP/types"; +import { createTransformTuplesReplyFunc, transformDoubleReply } from "@redis/client/lib/commands/generic-transformers"; import { TuplesReply } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument) { - return ['FT.INFO', index]; + parseCommand(parser: CommandParser, index: RedisArgument) { + parser.push('FT.INFO', index); }, transformReply: { 2: transformV2Reply, diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts index 8644ca5201e..ee112118c95 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts @@ -3,19 +3,20 @@ import testUtils, { GLOBAL } from '../test-utils'; import { FT_AGGREGATE_STEPS } from './AGGREGATE'; import PROFILE_AGGREGATE from './PROFILE_AGGREGATE'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('PROFILE AGGREGATE', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - PROFILE_AGGREGATE.transformArguments('index', 'query'), + parseArgs(PROFILE_AGGREGATE, 'index', 'query'), ['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query'] ); }); it('with options', () => { assert.deepEqual( - PROFILE_AGGREGATE.transformArguments('index', 'query', { + parseArgs(PROFILE_AGGREGATE, 'index', 'query', { LIMITED: true, VERBATIM: true, STEPS: [{ diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.ts index b6a8db38665..703bfcacc72 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.ts @@ -1,27 +1,26 @@ -// import { pushAggregatehOptions, AggregateOptions, transformReply as transformAggregateReply, AggregateRawReply } from './AGGREGATE'; -// import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.'; - -import { Command, ReplyUnion } from "@redis/client/dist/lib/RESP/types"; -import AGGREGATE, { AggregateRawReply, FtAggregateOptions, pushAggregateOptions } from "./AGGREGATE"; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, ReplyUnion } from "@redis/client/lib/RESP/types"; +import AGGREGATE, { AggregateRawReply, FtAggregateOptions, parseAggregateOptions } from "./AGGREGATE"; import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from "./PROFILE_SEARCH"; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, index: string, query: string, options?: ProfileOptions & FtAggregateOptions ) { - const args = ['FT.PROFILE', index, 'AGGREGATE']; + parser.push('FT.PROFILE', index, 'AGGREGATE'); if (options?.LIMITED) { - args.push('LIMITED'); + parser.push('LIMITED'); } - args.push('QUERY', query); + parser.push('QUERY', query); - return pushAggregateOptions(args, options) + parseAggregateOptions(parser, options) }, transformReply: { 2: (reply: ProfileAggeregateRawReply): ProfileReply => { diff --git a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts index a6e2a968d43..524ff1a5228 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts @@ -2,20 +2,21 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import PROFILE_SEARCH from './PROFILE_SEARCH'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('PROFILE SEARCH', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - PROFILE_SEARCH.transformArguments('index', 'query'), + parseArgs(PROFILE_SEARCH, 'index', 'query'), ['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query'] ); }); it('with options', () => { assert.deepEqual( - PROFILE_SEARCH.transformArguments('index', 'query', { + parseArgs(PROFILE_SEARCH, 'index', 'query', { LIMITED: true, VERBATIM: true, INKEYS: 'key' diff --git a/packages/search/lib/commands/PROFILE_SEARCH.ts b/packages/search/lib/commands/PROFILE_SEARCH.ts index 5b9e918083b..c345e70dd78 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.ts @@ -1,10 +1,7 @@ -// import { SearchOptions, SearchRawReply, transformReply as transformSearchReply } from './SEARCH'; -// import { pushSearchOptions, ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.'; -// import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; - -import { Command, RedisArgument, ReplyUnion } from "@redis/client/dist/lib/RESP/types"; -import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, pushSearchOptions } from "./SEARCH"; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, RedisArgument, ReplyUnion } from "@redis/client/lib/RESP/types"; import { AggregateReply } from "./AGGREGATE"; +import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, parseSearchOptions } from "./SEARCH"; export type ProfileRawReply = [ results: T, @@ -27,22 +24,23 @@ export interface ProfileOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: ProfileOptions & FtSearchOptions ) { - let args: Array = ['FT.PROFILE', index, 'SEARCH']; + parser.push('FT.PROFILE', index, 'SEARCH'); if (options?.LIMITED) { - args.push('LIMITED'); + parser.push('LIMITED'); } - args.push('QUERY', query); + parser.push('QUERY', query); - return pushSearchOptions(args, options); + parseSearchOptions(parser, options); }, transformReply: { 2: (reply: ProfileSearchRawReply, withoutDocuments: boolean): ProfileReply => { diff --git a/packages/search/lib/commands/SEARCH.spec.ts b/packages/search/lib/commands/SEARCH.spec.ts index 257dbb79515..24248b4cf15 100644 --- a/packages/search/lib/commands/SEARCH.spec.ts +++ b/packages/search/lib/commands/SEARCH.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SEARCH from './SEARCH'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SEARCH', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query'), + parseArgs(SEARCH, 'index', 'query'), ['FT.SEARCH', 'index', 'query'] ); }); it('with VERBATIM', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { VERBATIM: true }), ['FT.SEARCH', 'index', 'query', 'VERBATIM'] @@ -22,7 +23,7 @@ describe('FT.SEARCH', () => { it('with NOSTOPWORDS', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { NOSTOPWORDS: true }), ['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS'] @@ -31,7 +32,7 @@ describe('FT.SEARCH', () => { it('with INKEYS', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { INKEYS: 'key' }), ['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key'] @@ -40,7 +41,7 @@ describe('FT.SEARCH', () => { it('with INFIELDS', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { INFIELDS: 'field' }), ['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field'] @@ -49,7 +50,7 @@ describe('FT.SEARCH', () => { it('with RETURN', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { RETURN: 'return' }), ['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return'] @@ -59,7 +60,7 @@ describe('FT.SEARCH', () => { describe('with SUMMARIZE', () => { it('true', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { SUMMARIZE: true }), ['FT.SEARCH', 'index', 'query', 'SUMMARIZE'] @@ -69,7 +70,7 @@ describe('FT.SEARCH', () => { describe('with FIELDS', () => { it('string', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { SUMMARIZE: { FIELDS: '@field' } @@ -80,7 +81,7 @@ describe('FT.SEARCH', () => { it('Array', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { SUMMARIZE: { FIELDS: ['@1', '@2'] } @@ -92,7 +93,7 @@ describe('FT.SEARCH', () => { it('with FRAGS', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { SUMMARIZE: { FRAGS: 1 } @@ -103,7 +104,7 @@ describe('FT.SEARCH', () => { it('with LEN', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { SUMMARIZE: { LEN: 1 } @@ -114,7 +115,7 @@ describe('FT.SEARCH', () => { it('with SEPARATOR', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { SUMMARIZE: { SEPARATOR: 'separator' } @@ -127,7 +128,7 @@ describe('FT.SEARCH', () => { describe('with HIGHLIGHT', () => { it('true', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { HIGHLIGHT: true }), ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT'] @@ -137,7 +138,7 @@ describe('FT.SEARCH', () => { describe('with FIELDS', () => { it('string', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { HIGHLIGHT: { FIELDS: ['@field'] } @@ -148,7 +149,7 @@ describe('FT.SEARCH', () => { it('Array', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { HIGHLIGHT: { FIELDS: ['@1', '@2'] } @@ -160,7 +161,7 @@ describe('FT.SEARCH', () => { it('with TAGS', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { HIGHLIGHT: { TAGS: { open: 'open', @@ -175,7 +176,7 @@ describe('FT.SEARCH', () => { it('with SLOP', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { SLOP: 1 }), ['FT.SEARCH', 'index', 'query', 'SLOP', '1'] @@ -184,7 +185,7 @@ describe('FT.SEARCH', () => { it('with TIMEOUT', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { TIMEOUT: 1 }), ['FT.SEARCH', 'index', 'query', 'TIMEOUT', '1'] @@ -193,7 +194,7 @@ describe('FT.SEARCH', () => { it('with INORDER', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { INORDER: true }), ['FT.SEARCH', 'index', 'query', 'INORDER'] @@ -202,7 +203,7 @@ describe('FT.SEARCH', () => { it('with LANGUAGE', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { LANGUAGE: 'Arabic' }), ['FT.SEARCH', 'index', 'query', 'LANGUAGE', 'Arabic'] @@ -211,7 +212,7 @@ describe('FT.SEARCH', () => { it('with EXPANDER', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { EXPANDER: 'expender' }), ['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender'] @@ -220,7 +221,7 @@ describe('FT.SEARCH', () => { it('with SCORER', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { SCORER: 'scorer' }), ['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer'] @@ -229,7 +230,7 @@ describe('FT.SEARCH', () => { it('with SORTBY', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { SORTBY: '@by' }), ['FT.SEARCH', 'index', 'query', 'SORTBY', '@by'] @@ -238,7 +239,7 @@ describe('FT.SEARCH', () => { it('with LIMIT', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { LIMIT: { from: 0, size: 1 @@ -250,7 +251,7 @@ describe('FT.SEARCH', () => { it('with PARAMS', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { PARAMS: { string: 'string', buffer: Buffer.from('buffer'), @@ -263,7 +264,7 @@ describe('FT.SEARCH', () => { it('with DIALECT', () => { assert.deepEqual( - SEARCH.transformArguments('index', 'query', { + parseArgs(SEARCH, 'index', 'query', { DIALECT: 1 }), ['FT.SEARCH', 'index', 'query', 'DIALECT', '1'] diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index 1e5e8ec91f5..de03ac0070e 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -1,12 +1,15 @@ -import { RedisArgument, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushOptionalVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command, ReplyUnion } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; import { RediSearchProperty, RediSearchLanguage } from './CREATE'; export type FtSearchParams = Record; -export function pushParamsArgument(args: Array, params?: FtSearchParams) { +export function parseParamsArgument(parser: CommandParser, params?: FtSearchParams) { if (params) { - const length = args.push('PARAMS', ''); + parser.push('PARAMS'); + + const args: Array = []; for (const key in params) { if (!Object.hasOwn(params, key)) continue; @@ -17,7 +20,7 @@ export function pushParamsArgument(args: Array, params?: FtSearch ); } - args[length - 1] = (args.length - length).toString(); + parser.pushVariadicWithLength(args); } } @@ -58,109 +61,107 @@ export interface FtSearchOptions { DIALECT?: number; } -export function pushSearchOptions(args: Array, options?: FtSearchOptions) { +export function parseSearchOptions(parser: CommandParser, options?: FtSearchOptions) { if (options?.VERBATIM) { - args.push('VERBATIM'); + parser.push('VERBATIM'); } if (options?.NOSTOPWORDS) { - args.push('NOSTOPWORDS'); + parser.push('NOSTOPWORDS'); } - pushOptionalVariadicArgument(args, 'INKEYS', options?.INKEYS); - pushOptionalVariadicArgument(args, 'INFIELDS', options?.INFIELDS); - pushOptionalVariadicArgument(args, 'RETURN', options?.RETURN); + parseOptionalVariadicArgument(parser, 'INKEYS', options?.INKEYS); + parseOptionalVariadicArgument(parser, 'INFIELDS', options?.INFIELDS); + parseOptionalVariadicArgument(parser, 'RETURN', options?.RETURN); if (options?.SUMMARIZE) { - args.push('SUMMARIZE'); + parser.push('SUMMARIZE'); if (typeof options.SUMMARIZE === 'object') { - pushOptionalVariadicArgument(args, 'FIELDS', options.SUMMARIZE.FIELDS); + parseOptionalVariadicArgument(parser, 'FIELDS', options.SUMMARIZE.FIELDS); if (options.SUMMARIZE.FRAGS !== undefined) { - args.push('FRAGS', options.SUMMARIZE.FRAGS.toString()); + parser.push('FRAGS', options.SUMMARIZE.FRAGS.toString()); } if (options.SUMMARIZE.LEN !== undefined) { - args.push('LEN', options.SUMMARIZE.LEN.toString()); + parser.push('LEN', options.SUMMARIZE.LEN.toString()); } if (options.SUMMARIZE.SEPARATOR !== undefined) { - args.push('SEPARATOR', options.SUMMARIZE.SEPARATOR); + parser.push('SEPARATOR', options.SUMMARIZE.SEPARATOR); } } } if (options?.HIGHLIGHT) { - args.push('HIGHLIGHT'); + parser.push('HIGHLIGHT'); if (typeof options.HIGHLIGHT === 'object') { - pushOptionalVariadicArgument(args, 'FIELDS', options.HIGHLIGHT.FIELDS); + parseOptionalVariadicArgument(parser, 'FIELDS', options.HIGHLIGHT.FIELDS); if (options.HIGHLIGHT.TAGS) { - args.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close); + parser.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close); } } } if (options?.SLOP !== undefined) { - args.push('SLOP', options.SLOP.toString()); + parser.push('SLOP', options.SLOP.toString()); } if (options?.TIMEOUT !== undefined) { - args.push('TIMEOUT', options.TIMEOUT.toString()); + parser.push('TIMEOUT', options.TIMEOUT.toString()); } if (options?.INORDER) { - args.push('INORDER'); + parser.push('INORDER'); } if (options?.LANGUAGE) { - args.push('LANGUAGE', options.LANGUAGE); + parser.push('LANGUAGE', options.LANGUAGE); } if (options?.EXPANDER) { - args.push('EXPANDER', options.EXPANDER); + parser.push('EXPANDER', options.EXPANDER); } if (options?.SCORER) { - args.push('SCORER', options.SCORER); + parser.push('SCORER', options.SCORER); } if (options?.SORTBY) { - args.push('SORTBY'); + parser.push('SORTBY'); if (typeof options.SORTBY === 'string' || options.SORTBY instanceof Buffer) { - args.push(options.SORTBY); + parser.push(options.SORTBY); } else { - args.push(options.SORTBY.BY); + parser.push(options.SORTBY.BY); if (options.SORTBY.DIRECTION) { - args.push(options.SORTBY.DIRECTION); + parser.push(options.SORTBY.DIRECTION); } } } if (options?.LIMIT) { - args.push('LIMIT', options.LIMIT.from.toString(), options.LIMIT.size.toString()); + parser.push('LIMIT', options.LIMIT.from.toString(), options.LIMIT.size.toString()); } - pushParamsArgument(args, options?.PARAMS); + parseParamsArgument(parser, options?.PARAMS); if (options?.DIALECT !== undefined) { - args.push('DIALECT', options.DIALECT.toString()); + parser.push('DIALECT', options.DIALECT.toString()); } - - return args; } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument, query: RedisArgument, options?: FtSearchOptions) { - const args = ['FT.SEARCH', index, query]; + parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSearchOptions) { + parser.push('FT.SEARCH', index, query); - return pushSearchOptions(args, options); + parseSearchOptions(parser, options); }, transformReply: { 2: (reply: SearchRawReply): SearchReply => { diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts index be998b9e63d..bfcca8b4bda 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts @@ -1,12 +1,13 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import SEARCH_NOCONTENT from './SEARCH_NOCONTENT'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SEARCH NOCONTENT', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - SEARCH_NOCONTENT.transformArguments('index', 'query'), + parseArgs(SEARCH_NOCONTENT, 'index', 'query'), ['FT.SEARCH', 'index', 'query', 'NOCONTENT'] ); }); diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.ts index 4ee959b9d71..a4c6f6a27aa 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.ts @@ -1,13 +1,12 @@ -import { Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; +import { Command, ReplyUnion } from '@redis/client/lib/RESP/types'; import SEARCH, { SearchRawReply } from './SEARCH'; export default { - FIRST_KEY_INDEX: SEARCH.FIRST_KEY_INDEX, + NOT_KEYED_COMMAND: SEARCH.NOT_KEYED_COMMAND, IS_READ_ONLY: SEARCH.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const redisArgs = SEARCH.transformArguments(...args); - redisArgs.push('NOCONTENT'); - return redisArgs; + parseCommand(...args: Parameters) { + SEARCH.parseCommand(...args); + args[0].push('NOCONTENT'); }, transformReply: { 2: (reply: SearchRawReply): SearchNoContentReply => { diff --git a/packages/search/lib/commands/SPELLCHECK.spec.ts b/packages/search/lib/commands/SPELLCHECK.spec.ts index a70ee964920..4f5a3628f4d 100644 --- a/packages/search/lib/commands/SPELLCHECK.spec.ts +++ b/packages/search/lib/commands/SPELLCHECK.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SPELLCHECK from './SPELLCHECK'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SPELLCHECK', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - SPELLCHECK.transformArguments('index', 'query'), + parseArgs(SPELLCHECK, 'index', 'query'), ['FT.SPELLCHECK', 'index', 'query'] ); }); it('with DISTANCE', () => { assert.deepEqual( - SPELLCHECK.transformArguments('index', 'query', { + parseArgs(SPELLCHECK, 'index', 'query', { DISTANCE: 2 }), ['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2'] @@ -23,7 +24,7 @@ describe('FT.SPELLCHECK', () => { describe('with TERMS', () => { it('single', () => { assert.deepEqual( - SPELLCHECK.transformArguments('index', 'query', { + parseArgs(SPELLCHECK, 'index', 'query', { TERMS: { mode: 'INCLUDE', dictionary: 'dictionary' @@ -35,7 +36,7 @@ describe('FT.SPELLCHECK', () => { it('multiple', () => { assert.deepEqual( - SPELLCHECK.transformArguments('index', 'query', { + parseArgs(SPELLCHECK, 'index', 'query', { TERMS: [{ mode: 'INCLUDE', dictionary: 'include' @@ -51,7 +52,7 @@ describe('FT.SPELLCHECK', () => { it('with DIALECT', () => { assert.deepEqual( - SPELLCHECK.transformArguments('index', 'query', { + parseArgs(SPELLCHECK, 'index', 'query', { DIALECT: 1 }), ['FT.SPELLCHECK', 'index', 'query', 'DIALECT', '1'] diff --git a/packages/search/lib/commands/SPELLCHECK.ts b/packages/search/lib/commands/SPELLCHECK.ts index f52e74ba0f6..1e6981d01ff 100644 --- a/packages/search/lib/commands/SPELLCHECK.ts +++ b/packages/search/lib/commands/SPELLCHECK.ts @@ -1,4 +1,5 @@ -import { RedisArgument, CommandArguments, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command, ReplyUnion } from '@redis/client/lib/RESP/types'; export interface Terms { mode: 'INCLUDE' | 'EXCLUDE'; @@ -12,30 +13,28 @@ export interface FtSpellCheckOptions { } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument, query: RedisArgument, options?: FtSpellCheckOptions) { - const args = ['FT.SPELLCHECK', index, query]; + parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument, options?: FtSpellCheckOptions) { + parser.push('FT.SPELLCHECK', index, query); if (options?.DISTANCE) { - args.push('DISTANCE', options.DISTANCE.toString()); + parser.push('DISTANCE', options.DISTANCE.toString()); } if (options?.TERMS) { if (Array.isArray(options.TERMS)) { for (const term of options.TERMS) { - pushTerms(args, term); + parseTerms(parser, term); } } else { - pushTerms(args, options.TERMS); + parseTerms(parser, options.TERMS); } } if (options?.DIALECT) { - args.push('DIALECT', options.DIALECT.toString()); + parser.push('DIALECT', options.DIALECT.toString()); } - - return args; }, transformReply: { 2: (rawReply: SpellCheckRawReply): SpellCheckReply => { @@ -52,6 +51,10 @@ export default { unstableResp3: true } as const satisfies Command; +function parseTerms(parser: CommandParser, { mode, dictionary }: Terms) { + parser.push('TERMS', mode, dictionary); +} + type SpellCheckRawReply = Array<[ _: string, term: string, @@ -65,7 +68,3 @@ type SpellCheckReply = Array<{ suggestion: string }> }>; - -function pushTerms(args: CommandArguments, { mode, dictionary }: Terms) { - args.push('TERMS', mode, dictionary); -} diff --git a/packages/search/lib/commands/SUGADD.spec.ts b/packages/search/lib/commands/SUGADD.spec.ts index 24e03d37796..2e0ce92edbc 100644 --- a/packages/search/lib/commands/SUGADD.spec.ts +++ b/packages/search/lib/commands/SUGADD.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SUGADD from './SUGADD'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SUGADD', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - SUGADD.transformArguments('key', 'string', 1), + parseArgs(SUGADD, 'key', 'string', 1), ['FT.SUGADD', 'key', 'string', '1'] ); }); it('with INCR', () => { assert.deepEqual( - SUGADD.transformArguments('key', 'string', 1, { INCR: true }), + parseArgs(SUGADD, 'key', 'string', 1, { INCR: true }), ['FT.SUGADD', 'key', 'string', '1', 'INCR'] ); }); it('with PAYLOAD', () => { assert.deepEqual( - SUGADD.transformArguments('key', 'string', 1, { PAYLOAD: 'payload' }), + parseArgs(SUGADD, 'key', 'string', 1, { PAYLOAD: 'payload' }), ['FT.SUGADD', 'key', 'string', '1', 'PAYLOAD', 'payload'] ); }); diff --git a/packages/search/lib/commands/SUGADD.ts b/packages/search/lib/commands/SUGADD.ts index c18cd7846ed..a82f03ffa1b 100644 --- a/packages/search/lib/commands/SUGADD.ts +++ b/packages/search/lib/commands/SUGADD.ts @@ -1,4 +1,5 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; export interface FtSugAddOptions { INCR?: boolean; @@ -6,20 +7,19 @@ export interface FtSugAddOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, string: RedisArgument, score: number, options?: FtSugAddOptions) { - const args = ['FT.SUGADD', key, string, score.toString()]; + parseCommand(parser: CommandParser, key: RedisArgument, string: RedisArgument, score: number, options?: FtSugAddOptions) { + parser.push('FT.SUGADD'); + parser.pushKey(key); + parser.push(string, score.toString()); if (options?.INCR) { - args.push('INCR'); + parser.push('INCR'); } if (options?.PAYLOAD) { - args.push('PAYLOAD', options.PAYLOAD); + parser.push('PAYLOAD', options.PAYLOAD); } - - return args; }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/search/lib/commands/SUGDEL.spec.ts b/packages/search/lib/commands/SUGDEL.spec.ts index ea92c2a1a49..21677f14213 100644 --- a/packages/search/lib/commands/SUGDEL.spec.ts +++ b/packages/search/lib/commands/SUGDEL.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SUGDEL from './SUGDEL'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SUGDEL', () => { it('transformArguments', () => { assert.deepEqual( - SUGDEL.transformArguments('key', 'string'), + parseArgs(SUGDEL, 'key', 'string'), ['FT.SUGDEL', 'key', 'string'] ); }); diff --git a/packages/search/lib/commands/SUGDEL.ts b/packages/search/lib/commands/SUGDEL.ts index 5829ec40a2c..1cdf56d2025 100644 --- a/packages/search/lib/commands/SUGDEL.ts +++ b/packages/search/lib/commands/SUGDEL.ts @@ -1,10 +1,12 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, string: RedisArgument) { - return ['FT.SUGDEL', key, string]; + parseCommand(parser: CommandParser, key: RedisArgument, string: RedisArgument) { + parser.push('FT.SUGDEL'); + parser.pushKey(key); + parser.push(string); }, transformReply: undefined as unknown as () => NumberReply<0 | 1> } as const satisfies Command; diff --git a/packages/search/lib/commands/SUGGET.spec.ts b/packages/search/lib/commands/SUGGET.spec.ts index 6ea4c03f325..e30c62afd67 100644 --- a/packages/search/lib/commands/SUGGET.spec.ts +++ b/packages/search/lib/commands/SUGGET.spec.ts @@ -1,26 +1,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SUGGET from './SUGGET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SUGGET', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - SUGGET.transformArguments('key', 'prefix'), + parseArgs(SUGGET, 'key', 'prefix'), ['FT.SUGGET', 'key', 'prefix'] ); }); it('with FUZZY', () => { assert.deepEqual( - SUGGET.transformArguments('key', 'prefix', { FUZZY: true }), + parseArgs(SUGGET, 'key', 'prefix', { FUZZY: true }), ['FT.SUGGET', 'key', 'prefix', 'FUZZY'] ); }); it('with MAX', () => { assert.deepEqual( - SUGGET.transformArguments('key', 'prefix', { MAX: 10 }), + parseArgs(SUGGET, 'key', 'prefix', { MAX: 10 }), ['FT.SUGGET', 'key', 'prefix', 'MAX', '10'] ); }); diff --git a/packages/search/lib/commands/SUGGET.ts b/packages/search/lib/commands/SUGGET.ts index 53dc57a86aa..607e26df94e 100644 --- a/packages/search/lib/commands/SUGGET.ts +++ b/packages/search/lib/commands/SUGGET.ts @@ -1,4 +1,5 @@ -import { NullReply, ArrayReply, BlobStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { NullReply, ArrayReply, BlobStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; export interface FtSugGetOptions { FUZZY?: boolean; @@ -6,20 +7,19 @@ export interface FtSugGetOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, prefix: RedisArgument, options?: FtSugGetOptions) { - const args = ['FT.SUGGET', key, prefix]; + parseCommand(parser: CommandParser, key: RedisArgument, prefix: RedisArgument, options?: FtSugGetOptions) { + parser.push('FT.SUGGET'); + parser.pushKey(key); + parser.push(prefix); if (options?.FUZZY) { - args.push('FUZZY'); + parser.push('FUZZY'); } if (options?.MAX !== undefined) { - args.push('MAX', options.MAX.toString()); + parser.push('MAX', options.MAX.toString()); } - - return args; }, transformReply: undefined as unknown as () => NullReply | ArrayReply } as const satisfies Command; diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts index 42a427ce1f4..160d7e3eb7c 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SUGGET WITHPAYLOADS', () => { it('transformArguments', () => { assert.deepEqual( - SUGGET_WITHPAYLOADS.transformArguments('key', 'prefix'), + parseArgs(SUGGET_WITHPAYLOADS, 'key', 'prefix'), ['FT.SUGGET', 'key', 'prefix', 'WITHPAYLOADS'] ); }); diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts index d8b097f3dbc..b2112bb4b34 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts @@ -1,14 +1,12 @@ -import { NullReply, ArrayReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { isNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { NullReply, ArrayReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; +import { isNullReply } from '@redis/client/lib/commands/generic-transformers'; import SUGGET from './SUGGET'; export default { - FIRST_KEY_INDEX: SUGGET.FIRST_KEY_INDEX, IS_READ_ONLY: SUGGET.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const transformedArguments = SUGGET.transformArguments(...args); - transformedArguments.push('WITHPAYLOADS'); - return transformedArguments; + parseCommand(...args: Parameters) { + SUGGET.parseCommand(...args); + args[0].push('WITHPAYLOADS'); }, transformReply(reply: NullReply | UnwrapReply>) { if (isNullReply(reply)) return null; diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts index 6969be7729d..262defb7933 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SUGGET_WITHSCORES from './SUGGET_WITHSCORES'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SUGGET WITHSCORES', () => { it('transformArguments', () => { assert.deepEqual( - SUGGET_WITHSCORES.transformArguments('key', 'prefix'), + parseArgs(SUGGET_WITHSCORES, 'key', 'prefix'), ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES'] ); }); diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.ts index 9d24d95cbb0..088153c31f5 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.ts @@ -1,5 +1,5 @@ -import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types'; -import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/lib/RESP/types'; +import { isNullReply, transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; import SUGGET from './SUGGET'; type SuggestScore = { @@ -8,12 +8,10 @@ type SuggestScore = { } export default { - FIRST_KEY_INDEX: SUGGET.FIRST_KEY_INDEX, IS_READ_ONLY: SUGGET.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const transformedArguments = SUGGET.transformArguments(...args); - transformedArguments.push('WITHSCORES'); - return transformedArguments; + parseCommand(...args: Parameters) { + SUGGET.parseCommand(...args); + args[0].push('WITHSCORES'); }, transformReply: { 2: (reply: NullReply | UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts index 98aad1c8028..573708f689e 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SUGGET WITHSCORES WITHPAYLOADS', () => { it('transformArguments', () => { assert.deepEqual( - SUGGET_WITHSCORES_WITHPAYLOADS.transformArguments('key', 'prefix'), + parseArgs(SUGGET_WITHSCORES_WITHPAYLOADS, 'key', 'prefix'), ['FT.SUGGET', 'key', 'prefix', 'WITHSCORES', 'WITHPAYLOADS'] ); }); diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts index 1e125eb15fa..6f032a15899 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts @@ -1,5 +1,5 @@ -import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types'; -import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/lib/RESP/types'; +import { isNullReply, transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; import SUGGET from './SUGGET'; type SuggestScoreWithPayload = { @@ -9,15 +9,13 @@ type SuggestScoreWithPayload = { } export default { - FIRST_KEY_INDEX: SUGGET.FIRST_KEY_INDEX, IS_READ_ONLY: SUGGET.IS_READ_ONLY, - transformArguments(...args: Parameters) { - const transformedArguments = SUGGET.transformArguments(...args); - transformedArguments.push( + parseCommand(...args: Parameters) { + SUGGET.parseCommand(...args); + args[0].push( 'WITHSCORES', 'WITHPAYLOADS' ); - return transformedArguments; }, transformReply: { 2: (reply: NullReply | UnwrapReply>, preserve?: any, typeMapping?: TypeMapping) => { diff --git a/packages/search/lib/commands/SUGLEN.spec.ts b/packages/search/lib/commands/SUGLEN.spec.ts index 6e6d5e1fc5c..d738f09042e 100644 --- a/packages/search/lib/commands/SUGLEN.spec.ts +++ b/packages/search/lib/commands/SUGLEN.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SUGLEN from './SUGLEN'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SUGLEN', () => { it('transformArguments', () => { assert.deepEqual( - SUGLEN.transformArguments('key'), + parseArgs(SUGLEN, 'key'), ['FT.SUGLEN', 'key'] ); }); diff --git a/packages/search/lib/commands/SUGLEN.ts b/packages/search/lib/commands/SUGLEN.ts index 85dde8cfb70..7437559843f 100644 --- a/packages/search/lib/commands/SUGLEN.ts +++ b/packages/search/lib/commands/SUGLEN.ts @@ -1,10 +1,10 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument) { - return ['FT.SUGLEN', key]; + parseCommand(parser: CommandParser, key: RedisArgument) { + parser.push('FT.SUGLEN', key); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/search/lib/commands/SYNDUMP.spec.ts b/packages/search/lib/commands/SYNDUMP.spec.ts index 59c010a8d6d..88bf50cfb54 100644 --- a/packages/search/lib/commands/SYNDUMP.spec.ts +++ b/packages/search/lib/commands/SYNDUMP.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SYNDUMP from './SYNDUMP'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SYNDUMP', () => { it('transformArguments', () => { assert.deepEqual( - SYNDUMP.transformArguments('index'), + parseArgs(SYNDUMP, 'index'), ['FT.SYNDUMP', 'index'] ); }); diff --git a/packages/search/lib/commands/SYNDUMP.ts b/packages/search/lib/commands/SYNDUMP.ts index 2fe7540fda5..0c3b68bf2e7 100644 --- a/packages/search/lib/commands/SYNDUMP.ts +++ b/packages/search/lib/commands/SYNDUMP.ts @@ -1,10 +1,11 @@ -import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument) { - return ['FT.SYNDUMP', index]; + parseCommand(parser: CommandParser, index: RedisArgument) { + parser.push('FT.SYNDUMP', index); }, transformReply: { 2: (reply: UnwrapReply>>) => { diff --git a/packages/search/lib/commands/SYNUPDATE.spec.ts b/packages/search/lib/commands/SYNUPDATE.spec.ts index e901ae9fe3f..f93e0599151 100644 --- a/packages/search/lib/commands/SYNUPDATE.spec.ts +++ b/packages/search/lib/commands/SYNUPDATE.spec.ts @@ -2,26 +2,27 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SYNUPDATE from './SYNUPDATE'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.SYNUPDATE', () => { describe('transformArguments', () => { it('single term', () => { assert.deepEqual( - SYNUPDATE.transformArguments('index', 'groupId', 'term'), + parseArgs(SYNUPDATE, 'index', 'groupId', 'term'), ['FT.SYNUPDATE', 'index', 'groupId', 'term'] ); }); it('multiple terms', () => { assert.deepEqual( - SYNUPDATE.transformArguments('index', 'groupId', ['1', '2']), + parseArgs(SYNUPDATE, 'index', 'groupId', ['1', '2']), ['FT.SYNUPDATE', 'index', 'groupId', '1', '2'] ); }); it('with SKIPINITIALSCAN', () => { assert.deepEqual( - SYNUPDATE.transformArguments('index', 'groupId', 'term', { + parseArgs(SYNUPDATE, 'index', 'groupId', 'term', { SKIPINITIALSCAN: true }), ['FT.SYNUPDATE', 'index', 'groupId', 'SKIPINITIALSCAN', 'term'] diff --git a/packages/search/lib/commands/SYNUPDATE.ts b/packages/search/lib/commands/SYNUPDATE.ts index 926d8e58e1c..2baf2ded18c 100644 --- a/packages/search/lib/commands/SYNUPDATE.ts +++ b/packages/search/lib/commands/SYNUPDATE.ts @@ -1,26 +1,28 @@ -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export interface FtSynUpdateOptions { SKIPINITIALSCAN?: boolean; } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments( + parseCommand( + parser: CommandParser, index: RedisArgument, groupId: RedisArgument, terms: RedisVariadicArgument, options?: FtSynUpdateOptions ) { - const args = ['FT.SYNUPDATE', index, groupId]; + parser.push('FT.SYNUPDATE', index, groupId); if (options?.SKIPINITIALSCAN) { - args.push('SKIPINITIALSCAN'); + parser.push('SKIPINITIALSCAN'); } - return pushVariadicArguments(args, terms); + parser.pushVariadic(terms); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/search/lib/commands/TAGVALS.spec.ts b/packages/search/lib/commands/TAGVALS.spec.ts index dbc6203f93e..f0d83c9f7ad 100644 --- a/packages/search/lib/commands/TAGVALS.spec.ts +++ b/packages/search/lib/commands/TAGVALS.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import TAGVALS from './TAGVALS'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('FT.TAGVALS', () => { it('transformArguments', () => { assert.deepEqual( - TAGVALS.transformArguments('index', '@field'), + parseArgs(TAGVALS, 'index', '@field'), ['FT.TAGVALS', 'index', '@field'] ); }); diff --git a/packages/search/lib/commands/TAGVALS.ts b/packages/search/lib/commands/TAGVALS.ts index 8a6e73c97b8..d00d657f3ab 100644 --- a/packages/search/lib/commands/TAGVALS.ts +++ b/packages/search/lib/commands/TAGVALS.ts @@ -1,10 +1,11 @@ -import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(index: RedisArgument, fieldName: RedisArgument) { - return ['FT.TAGVALS', index, fieldName]; + parseCommand(parser: CommandParser, index: RedisArgument, fieldName: RedisArgument) { + parser.push('FT.TAGVALS', index, fieldName); }, transformReply: { 2: undefined as unknown as () => ArrayReply, diff --git a/packages/search/lib/commands/_LIST.spec.ts b/packages/search/lib/commands/_LIST.spec.ts index a7f13b011ac..dfe32f2e29d 100644 --- a/packages/search/lib/commands/_LIST.spec.ts +++ b/packages/search/lib/commands/_LIST.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import _LIST from './_LIST'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('_LIST', () => { it('transformArguments', () => { assert.deepEqual( - _LIST.transformArguments(), + parseArgs(_LIST), ['FT._LIST'] ); }); diff --git a/packages/search/lib/commands/_LIST.ts b/packages/search/lib/commands/_LIST.ts index efb6c31acce..432eea64797 100644 --- a/packages/search/lib/commands/_LIST.ts +++ b/packages/search/lib/commands/_LIST.ts @@ -1,10 +1,11 @@ -import { ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments() { - return ['FT._LIST']; + parseCommand(parser: CommandParser) { + parser.push('FT._LIST'); }, transformReply: { 2: undefined as unknown as () => ArrayReply, diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index a1cb63eb7bf..e8d7ad45ca6 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -2,7 +2,7 @@ import { createConnection } from 'node:net'; import { once } from 'node:events'; import { createClient } from '@redis/client/index'; import { setTimeout } from 'node:timers/promises'; -// import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS'; +// import { ClusterSlotsReply } from '@redis/client/lib/commands/CLUSTER_SLOTS'; import { promisify } from 'node:util'; import { exec } from 'node:child_process'; const execAsync = promisify(exec); diff --git a/packages/time-series/lib/commands/ADD.spec.ts b/packages/time-series/lib/commands/ADD.spec.ts index 7dcf031c2b2..055d2246d8b 100644 --- a/packages/time-series/lib/commands/ADD.spec.ts +++ b/packages/time-series/lib/commands/ADD.spec.ts @@ -2,19 +2,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ADD from './ADD'; import { TIME_SERIES_ENCODING, TIME_SERIES_DUPLICATE_POLICIES } from '.'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.ADD', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - ADD.transformArguments('key', '*', 1), + parseArgs(ADD, 'key', '*', 1), ['TS.ADD', 'key', '*', '1'] ); }); it('with RETENTION', () => { assert.deepEqual( - ADD.transformArguments('key', '*', 1, { + parseArgs(ADD, 'key', '*', 1, { RETENTION: 1 }), ['TS.ADD', 'key', '*', '1', 'RETENTION', '1'] @@ -23,7 +24,7 @@ describe('TS.ADD', () => { it('with ENCODING', () => { assert.deepEqual( - ADD.transformArguments('key', '*', 1, { + parseArgs(ADD, 'key', '*', 1, { ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED }), ['TS.ADD', 'key', '*', '1', 'ENCODING', 'UNCOMPRESSED'] @@ -32,7 +33,7 @@ describe('TS.ADD', () => { it('with CHUNK_SIZE', () => { assert.deepEqual( - ADD.transformArguments('key', '*', 1, { + parseArgs(ADD, 'key', '*', 1, { CHUNK_SIZE: 1 }), ['TS.ADD', 'key', '*', '1', 'CHUNK_SIZE', '1'] @@ -41,7 +42,7 @@ describe('TS.ADD', () => { it('with ON_DUPLICATE', () => { assert.deepEqual( - ADD.transformArguments('key', '*', 1, { + parseArgs(ADD, 'key', '*', 1, { ON_DUPLICATE: TIME_SERIES_DUPLICATE_POLICIES.BLOCK }), ['TS.ADD', 'key', '*', '1', 'ON_DUPLICATE', 'BLOCK'] @@ -50,7 +51,7 @@ describe('TS.ADD', () => { it('with LABELS', () => { assert.deepEqual( - ADD.transformArguments('key', '*', 1, { + parseArgs(ADD, 'key', '*', 1, { LABELS: { label: 'value' } }), ['TS.ADD', 'key', '*', '1', 'LABELS', 'label', 'value'] @@ -59,7 +60,7 @@ describe('TS.ADD', () => { it ('with IGNORE', () => { assert.deepEqual( - ADD.transformArguments('key', '*', 1, { + parseArgs(ADD, 'key', '*', 1, { IGNORE: { maxTimeDiff: 1, maxValDiff: 1 @@ -71,7 +72,7 @@ describe('TS.ADD', () => { it('with RETENTION, ENCODING, CHUNK_SIZE, ON_DUPLICATE, LABELS, IGNORE', () => { assert.deepEqual( - ADD.transformArguments('key', '*', 1, { + parseArgs(ADD, 'key', '*', 1, { RETENTION: 1, ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED, CHUNK_SIZE: 1, diff --git a/packages/time-series/lib/commands/ADD.ts b/packages/time-series/lib/commands/ADD.ts index 1842dcfc346..f79a27c5db7 100644 --- a/packages/time-series/lib/commands/ADD.ts +++ b/packages/time-series/lib/commands/ADD.ts @@ -1,15 +1,16 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; import { transformTimestampArgument, - pushRetentionArgument, + parseRetentionArgument, TimeSeriesEncoding, - pushEncodingArgument, - pushChunkSizeArgument, + parseEncodingArgument, + parseChunkSizeArgument, TimeSeriesDuplicatePolicies, Labels, - pushLabelsArgument, + parseLabelsArgument, Timestamp, - pushIgnoreArgument + parseIgnoreArgument } from '.'; export interface TsIgnoreOptions { @@ -27,36 +28,31 @@ export interface TsAddOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, key: RedisArgument, timestamp: Timestamp, value: number, options?: TsAddOptions ) { - const args = [ - 'TS.ADD', - key, - transformTimestampArgument(timestamp), - value.toString() - ]; + parser.push('TS.ADD'); + parser.pushKey(key); + parser.push(transformTimestampArgument(timestamp), value.toString()); - pushRetentionArgument(args, options?.RETENTION); + parseRetentionArgument(parser, options?.RETENTION); - pushEncodingArgument(args, options?.ENCODING); + parseEncodingArgument(parser, options?.ENCODING); - pushChunkSizeArgument(args, options?.CHUNK_SIZE); + parseChunkSizeArgument(parser, options?.CHUNK_SIZE); if (options?.ON_DUPLICATE) { - args.push('ON_DUPLICATE', options.ON_DUPLICATE); + parser.push('ON_DUPLICATE', options.ON_DUPLICATE); } - pushLabelsArgument(args, options?.LABELS); + parseLabelsArgument(parser, options?.LABELS); - pushIgnoreArgument(args, options?.IGNORE); - - return args; + parseIgnoreArgument(parser, options?.IGNORE); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/time-series/lib/commands/ALTER.spec.ts b/packages/time-series/lib/commands/ALTER.spec.ts index 1b24111156b..560d9ffde2c 100644 --- a/packages/time-series/lib/commands/ALTER.spec.ts +++ b/packages/time-series/lib/commands/ALTER.spec.ts @@ -2,19 +2,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import ALTER from './ALTER'; import { TIME_SERIES_DUPLICATE_POLICIES } from '.'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.ALTER', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - ALTER.transformArguments('key'), + parseArgs(ALTER, 'key'), ['TS.ALTER', 'key'] ); }); it('with RETENTION', () => { assert.deepEqual( - ALTER.transformArguments('key', { + parseArgs(ALTER, 'key', { RETENTION: 1 }), ['TS.ALTER', 'key', 'RETENTION', '1'] @@ -23,7 +24,7 @@ describe('TS.ALTER', () => { it('with CHUNK_SIZE', () => { assert.deepEqual( - ALTER.transformArguments('key', { + parseArgs(ALTER, 'key', { CHUNK_SIZE: 1 }), ['TS.ALTER', 'key', 'CHUNK_SIZE', '1'] @@ -32,7 +33,7 @@ describe('TS.ALTER', () => { it('with DUPLICATE_POLICY', () => { assert.deepEqual( - ALTER.transformArguments('key', { + parseArgs(ALTER, 'key', { DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK }), ['TS.ALTER', 'key', 'DUPLICATE_POLICY', 'BLOCK'] @@ -41,7 +42,7 @@ describe('TS.ALTER', () => { it('with LABELS', () => { assert.deepEqual( - ALTER.transformArguments('key', { + parseArgs(ALTER, 'key', { LABELS: { label: 'value' } }), ['TS.ALTER', 'key', 'LABELS', 'label', 'value'] @@ -50,7 +51,7 @@ describe('TS.ALTER', () => { it('with IGNORE with MAX_TIME_DIFF', () => { assert.deepEqual( - ALTER.transformArguments('key', { + parseArgs(ALTER, 'key', { IGNORE: { maxTimeDiff: 1, maxValDiff: 1 @@ -62,7 +63,7 @@ describe('TS.ALTER', () => { it('with RETENTION, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => { assert.deepEqual( - ALTER.transformArguments('key', { + parseArgs(ALTER, 'key', { RETENTION: 1, CHUNK_SIZE: 1, DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK, diff --git a/packages/time-series/lib/commands/ALTER.ts b/packages/time-series/lib/commands/ALTER.ts index f77edb5c43f..8217e81c218 100644 --- a/packages/time-series/lib/commands/ALTER.ts +++ b/packages/time-series/lib/commands/ALTER.ts @@ -1,26 +1,25 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; import { TsCreateOptions } from './CREATE'; -import { pushRetentionArgument, pushChunkSizeArgument, pushDuplicatePolicy, pushLabelsArgument, pushIgnoreArgument } from '.'; +import { parseRetentionArgument, parseChunkSizeArgument, parseDuplicatePolicy, parseLabelsArgument, parseIgnoreArgument } from '.'; export type TsAlterOptions = Pick; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: TsAlterOptions) { - const args = ['TS.ALTER', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: TsAlterOptions) { + parser.push('TS.ALTER'); + parser.pushKey(key); - pushRetentionArgument(args, options?.RETENTION); + parseRetentionArgument(parser, options?.RETENTION); - pushChunkSizeArgument(args, options?.CHUNK_SIZE); + parseChunkSizeArgument(parser, options?.CHUNK_SIZE); - pushDuplicatePolicy(args, options?.DUPLICATE_POLICY); + parseDuplicatePolicy(parser, options?.DUPLICATE_POLICY); - pushLabelsArgument(args, options?.LABELS); + parseLabelsArgument(parser, options?.LABELS); - pushIgnoreArgument(args, options?.IGNORE); - - return args; + parseIgnoreArgument(parser, options?.IGNORE); }, -transformReply: undefined as unknown as () => SimpleStringReply<'OK'> + transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/time-series/lib/commands/CREATE.spec.ts b/packages/time-series/lib/commands/CREATE.spec.ts index feff9cbdd7b..795b59b880d 100644 --- a/packages/time-series/lib/commands/CREATE.spec.ts +++ b/packages/time-series/lib/commands/CREATE.spec.ts @@ -2,19 +2,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CREATE from './CREATE'; import { TIME_SERIES_ENCODING, TIME_SERIES_DUPLICATE_POLICIES } from '.'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.CREATE', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - CREATE.transformArguments('key'), + parseArgs(CREATE, 'key'), ['TS.CREATE', 'key'] ); }); it('with RETENTION', () => { assert.deepEqual( - CREATE.transformArguments('key', { + parseArgs(CREATE, 'key', { RETENTION: 1 }), ['TS.CREATE', 'key', 'RETENTION', '1'] @@ -23,7 +24,7 @@ describe('TS.CREATE', () => { it('with ENCODING', () => { assert.deepEqual( - CREATE.transformArguments('key', { + parseArgs(CREATE, 'key', { ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED }), ['TS.CREATE', 'key', 'ENCODING', 'UNCOMPRESSED'] @@ -32,7 +33,7 @@ describe('TS.CREATE', () => { it('with CHUNK_SIZE', () => { assert.deepEqual( - CREATE.transformArguments('key', { + parseArgs(CREATE, 'key', { CHUNK_SIZE: 1 }), ['TS.CREATE', 'key', 'CHUNK_SIZE', '1'] @@ -41,7 +42,7 @@ describe('TS.CREATE', () => { it('with DUPLICATE_POLICY', () => { assert.deepEqual( - CREATE.transformArguments('key', { + parseArgs(CREATE, 'key', { DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK }), ['TS.CREATE', 'key', 'DUPLICATE_POLICY', 'BLOCK'] @@ -50,7 +51,7 @@ describe('TS.CREATE', () => { it('with LABELS', () => { assert.deepEqual( - CREATE.transformArguments('key', { + parseArgs(CREATE, 'key', { LABELS: { label: 'value' } }), ['TS.CREATE', 'key', 'LABELS', 'label', 'value'] @@ -59,7 +60,7 @@ describe('TS.CREATE', () => { it('with IGNORE with MAX_TIME_DIFF', () => { assert.deepEqual( - CREATE.transformArguments('key', { + parseArgs(CREATE, 'key', { IGNORE: { maxTimeDiff: 1, maxValDiff: 1 @@ -71,7 +72,7 @@ describe('TS.CREATE', () => { it('with RETENTION, ENCODING, CHUNK_SIZE, DUPLICATE_POLICY, LABELS, IGNORE', () => { assert.deepEqual( - CREATE.transformArguments('key', { + parseArgs(CREATE, 'key', { RETENTION: 1, ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED, CHUNK_SIZE: 1, diff --git a/packages/time-series/lib/commands/CREATE.ts b/packages/time-series/lib/commands/CREATE.ts index abb84de12a2..86defd1e0a4 100644 --- a/packages/time-series/lib/commands/CREATE.ts +++ b/packages/time-series/lib/commands/CREATE.ts @@ -1,14 +1,15 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; import { - pushRetentionArgument, + parseRetentionArgument, TimeSeriesEncoding, - pushEncodingArgument, - pushChunkSizeArgument, + parseEncodingArgument, + parseChunkSizeArgument, TimeSeriesDuplicatePolicies, - pushDuplicatePolicy, + parseDuplicatePolicy, Labels, - pushLabelsArgument, - pushIgnoreArgument + parseLabelsArgument, + parseIgnoreArgument } from '.'; import { TsIgnoreOptions } from './ADD'; @@ -22,24 +23,22 @@ export interface TsCreateOptions { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, options?: TsCreateOptions) { - const args = ['TS.CREATE', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: TsCreateOptions) { + parser.push('TS.CREATE'); + parser.pushKey(key); - pushRetentionArgument(args, options?.RETENTION); + parseRetentionArgument(parser, options?.RETENTION); - pushEncodingArgument(args, options?.ENCODING); + parseEncodingArgument(parser, options?.ENCODING); - pushChunkSizeArgument(args, options?.CHUNK_SIZE); + parseChunkSizeArgument(parser, options?.CHUNK_SIZE); - pushDuplicatePolicy(args, options?.DUPLICATE_POLICY); + parseDuplicatePolicy(parser, options?.DUPLICATE_POLICY); - pushLabelsArgument(args, options?.LABELS); + parseLabelsArgument(parser, options?.LABELS); - pushIgnoreArgument(args, options?.IGNORE); - - return args; + parseIgnoreArgument(parser, options?.IGNORE); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/time-series/lib/commands/CREATERULE.spec.ts b/packages/time-series/lib/commands/CREATERULE.spec.ts index f1e5b934506..da26bf458e2 100644 --- a/packages/time-series/lib/commands/CREATERULE.spec.ts +++ b/packages/time-series/lib/commands/CREATERULE.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import CREATERULE, { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.CREATERULE', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - CREATERULE.transformArguments('source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1), + parseArgs(CREATERULE, 'source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1), ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1'] ); }); it('with alignTimestamp', () => { assert.deepEqual( - CREATERULE.transformArguments('source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1, 1), + parseArgs(CREATERULE, 'source', 'destination', TIME_SERIES_AGGREGATION_TYPE.AVG, 1, 1), ['TS.CREATERULE', 'source', 'destination', 'AGGREGATION', 'AVG', '1', '1'] ); }); diff --git a/packages/time-series/lib/commands/CREATERULE.ts b/packages/time-series/lib/commands/CREATERULE.ts index bd074d7107c..99a8a4c9d57 100644 --- a/packages/time-series/lib/commands/CREATERULE.ts +++ b/packages/time-series/lib/commands/CREATERULE.ts @@ -1,4 +1,5 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export const TIME_SERIES_AGGREGATION_TYPE = { AVG: 'AVG', @@ -19,29 +20,22 @@ export const TIME_SERIES_AGGREGATION_TYPE = { export type TimeSeriesAggregationType = typeof TIME_SERIES_AGGREGATION_TYPE[keyof typeof TIME_SERIES_AGGREGATION_TYPE]; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments( + parseCommand( + parser: CommandParser, sourceKey: RedisArgument, destinationKey: RedisArgument, aggregationType: TimeSeriesAggregationType, bucketDuration: number, alignTimestamp?: number ) { - const args = [ - 'TS.CREATERULE', - sourceKey, - destinationKey, - 'AGGREGATION', - aggregationType, - bucketDuration.toString() - ]; + parser.push('TS.CREATERULE'); + parser.pushKeys([sourceKey, destinationKey]); + parser.push('AGGREGATION', aggregationType, bucketDuration.toString()); if (alignTimestamp !== undefined) { - args.push(alignTimestamp.toString()); + parser.push(alignTimestamp.toString()); } - - return args; }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/time-series/lib/commands/DECRBY.spec.ts b/packages/time-series/lib/commands/DECRBY.spec.ts index dbce98b2acd..b272ed1614d 100644 --- a/packages/time-series/lib/commands/DECRBY.spec.ts +++ b/packages/time-series/lib/commands/DECRBY.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DECRBY from './DECRBY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.DECRBY', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - DECRBY.transformArguments('key', 1), + parseArgs(DECRBY, 'key', 1), ['TS.DECRBY', 'key', '1'] ); }); it('with TIMESTAMP', () => { assert.deepEqual( - DECRBY.transformArguments('key', 1, { + parseArgs(DECRBY, 'key', 1, { TIMESTAMP: '*' }), ['TS.DECRBY', 'key', '1', 'TIMESTAMP', '*'] @@ -22,7 +23,7 @@ describe('TS.DECRBY', () => { it('with RETENTION', () => { assert.deepEqual( - DECRBY.transformArguments('key', 1, { + parseArgs(DECRBY, 'key', 1, { RETENTION: 1 }), ['TS.DECRBY', 'key', '1', 'RETENTION', '1'] @@ -31,7 +32,7 @@ describe('TS.DECRBY', () => { it('with UNCOMPRESSED', () => { assert.deepEqual( - DECRBY.transformArguments('key', 1, { + parseArgs(DECRBY, 'key', 1, { UNCOMPRESSED: true }), ['TS.DECRBY', 'key', '1', 'UNCOMPRESSED'] @@ -40,7 +41,7 @@ describe('TS.DECRBY', () => { it('with CHUNK_SIZE', () => { assert.deepEqual( - DECRBY.transformArguments('key', 1, { + parseArgs(DECRBY, 'key', 1, { CHUNK_SIZE: 100 }), ['TS.DECRBY', 'key', '1', 'CHUNK_SIZE', '100'] @@ -49,7 +50,7 @@ describe('TS.DECRBY', () => { it('with LABELS', () => { assert.deepEqual( - DECRBY.transformArguments('key', 1, { + parseArgs(DECRBY, 'key', 1, { LABELS: { label: 'value' } }), ['TS.DECRBY', 'key', '1', 'LABELS', 'label', 'value'] @@ -58,7 +59,7 @@ describe('TS.DECRBY', () => { it ('with IGNORE', () => { assert.deepEqual( - DECRBY.transformArguments('key', 1, { + parseArgs(DECRBY, 'key', 1, { IGNORE: { maxTimeDiff: 1, maxValDiff: 1 @@ -70,7 +71,7 @@ describe('TS.DECRBY', () => { it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => { assert.deepEqual( - DECRBY.transformArguments('key', 1, { + parseArgs(DECRBY, 'key', 1, { TIMESTAMP: '*', RETENTION: 1, UNCOMPRESSED: true, diff --git a/packages/time-series/lib/commands/DECRBY.ts b/packages/time-series/lib/commands/DECRBY.ts index a5ee01efb06..c2a7e6abd97 100644 --- a/packages/time-series/lib/commands/DECRBY.ts +++ b/packages/time-series/lib/commands/DECRBY.ts @@ -1,9 +1,13 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; -import INCRBY, { transformIncrByArguments } from './INCRBY'; +import { Command } from '@redis/client/lib/RESP/types'; +import INCRBY, { parseIncrByArguments } from './INCRBY'; export default { - FIRST_KEY_INDEX: INCRBY.FIRST_KEY_INDEX, IS_READ_ONLY: INCRBY.IS_READ_ONLY, - transformArguments: transformIncrByArguments.bind(undefined, 'TS.DECRBY'), + parseCommand(...args: Parameters) { + const parser = args[0]; + + parser.push('TS.DECRBY'); + parseIncrByArguments(...args); + }, transformReply: INCRBY.transformReply } as const satisfies Command; diff --git a/packages/time-series/lib/commands/DEL.spec.ts b/packages/time-series/lib/commands/DEL.spec.ts index afe6be77c4b..07d29ca095e 100644 --- a/packages/time-series/lib/commands/DEL.spec.ts +++ b/packages/time-series/lib/commands/DEL.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DEL from './DEL'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.DEL', () => { it('transformArguments', () => { assert.deepEqual( - DEL.transformArguments('key', '-', '+'), + parseArgs(DEL, 'key', '-', '+'), ['TS.DEL', 'key', '-', '+'] ); }); diff --git a/packages/time-series/lib/commands/DEL.ts b/packages/time-series/lib/commands/DEL.ts index 26c3e610f17..ad957e6c402 100644 --- a/packages/time-series/lib/commands/DEL.ts +++ b/packages/time-series/lib/commands/DEL.ts @@ -1,16 +1,13 @@ +import { CommandParser } from '@redis/client/lib/client/parser'; import { Timestamp, transformTimestampArgument } from '.'; -import { RedisArgument, NumberReply, Command, } from '@redis/client/dist/lib/RESP/types'; +import { RedisArgument, NumberReply, Command, } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(key: RedisArgument, fromTimestamp: Timestamp, toTimestamp: Timestamp) { - return [ - 'TS.DEL', - key, - transformTimestampArgument(fromTimestamp), - transformTimestampArgument(toTimestamp) - ]; + parseCommand(parser: CommandParser, key: RedisArgument, fromTimestamp: Timestamp, toTimestamp: Timestamp) { + parser.push('TS.DEL'); + parser.pushKey(key); + parser.push(transformTimestampArgument(fromTimestamp), transformTimestampArgument(toTimestamp)); }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/time-series/lib/commands/DELETERULE.spec.ts b/packages/time-series/lib/commands/DELETERULE.spec.ts index 8c8568c8567..d7a19a8eaa1 100644 --- a/packages/time-series/lib/commands/DELETERULE.spec.ts +++ b/packages/time-series/lib/commands/DELETERULE.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import DELETERULE from './DELETERULE'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.DELETERULE', () => { it('transformArguments', () => { assert.deepEqual( - DELETERULE.transformArguments('source', 'destination'), + parseArgs(DELETERULE, 'source', 'destination'), ['TS.DELETERULE', 'source', 'destination'] ); }); diff --git a/packages/time-series/lib/commands/DELETERULE.ts b/packages/time-series/lib/commands/DELETERULE.ts index 5cf88897f7d..8a1aa41385d 100644 --- a/packages/time-series/lib/commands/DELETERULE.ts +++ b/packages/time-series/lib/commands/DELETERULE.ts @@ -1,14 +1,11 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(sourceKey: RedisArgument, destinationKey: RedisArgument) { - return [ - 'TS.DELETERULE', - sourceKey, - destinationKey - ]; + parseCommand(parser: CommandParser, sourceKey: RedisArgument, destinationKey: RedisArgument) { + parser.push('TS.DELETERULE'); + parser.pushKeys([sourceKey, destinationKey]); }, transformReply: undefined as unknown as () => SimpleStringReply<'OK'> } as const satisfies Command; diff --git a/packages/time-series/lib/commands/GET.spec.ts b/packages/time-series/lib/commands/GET.spec.ts index a1f47346bc2..836a1b638af 100644 --- a/packages/time-series/lib/commands/GET.spec.ts +++ b/packages/time-series/lib/commands/GET.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GET from './GET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.GET', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - GET.transformArguments('key'), + parseArgs(GET, 'key'), ['TS.GET', 'key'] ); }); it('with LATEST', () => { assert.deepEqual( - GET.transformArguments('key', { + parseArgs(GET, 'key', { LATEST: true }), ['TS.GET', 'key', 'LATEST'] diff --git a/packages/time-series/lib/commands/GET.ts b/packages/time-series/lib/commands/GET.ts index 78e5e3bced0..9f165bed6eb 100644 --- a/packages/time-series/lib/commands/GET.ts +++ b/packages/time-series/lib/commands/GET.ts @@ -1,4 +1,5 @@ -import { RedisArgument, TuplesReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, TuplesReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command } from '@redis/client/lib/RESP/types'; export interface TsGetOptions { LATEST?: boolean; @@ -7,16 +8,14 @@ export interface TsGetOptions { export type TsGetReply = TuplesReply<[]> | TuplesReply<[NumberReply, DoubleReply]>; export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: RedisArgument, options?: TsGetOptions) { - const args = ['TS.GET', key]; + parseCommand(parser: CommandParser, key: RedisArgument, options?: TsGetOptions) { + parser.push('TS.GET'); + parser.pushKey(key); if (options?.LATEST) { - args.push('LATEST'); + parser.push('LATEST'); } - - return args; }, transformReply: { 2(reply: UnwrapReply>) { diff --git a/packages/time-series/lib/commands/INCRBY.spec.ts b/packages/time-series/lib/commands/INCRBY.spec.ts index 33163a72c82..5d005952b30 100644 --- a/packages/time-series/lib/commands/INCRBY.spec.ts +++ b/packages/time-series/lib/commands/INCRBY.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import INCRBY from './INCRBY'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.INCRBY', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1), + parseArgs(INCRBY, 'key', 1), ['TS.INCRBY', 'key', '1'] ); }); it('with TIMESTAMP', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1, { + parseArgs(INCRBY, 'key', 1, { TIMESTAMP: '*' }), ['TS.INCRBY', 'key', '1', 'TIMESTAMP', '*'] @@ -22,7 +23,7 @@ describe('TS.INCRBY', () => { it('with RETENTION', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1, { + parseArgs(INCRBY, 'key', 1, { RETENTION: 1 }), ['TS.INCRBY', 'key', '1', 'RETENTION', '1'] @@ -31,7 +32,7 @@ describe('TS.INCRBY', () => { it('with UNCOMPRESSED', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1, { + parseArgs(INCRBY, 'key', 1, { UNCOMPRESSED: true }), ['TS.INCRBY', 'key', '1', 'UNCOMPRESSED'] @@ -40,7 +41,7 @@ describe('TS.INCRBY', () => { it('without UNCOMPRESSED', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1, { + parseArgs(INCRBY, 'key', 1, { UNCOMPRESSED: false }), ['TS.INCRBY', 'key', '1'] @@ -49,7 +50,7 @@ describe('TS.INCRBY', () => { it('with CHUNK_SIZE', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1, { + parseArgs(INCRBY, 'key', 1, { CHUNK_SIZE: 1 }), ['TS.INCRBY', 'key', '1', 'CHUNK_SIZE', '1'] @@ -58,7 +59,7 @@ describe('TS.INCRBY', () => { it('with LABELS', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1, { + parseArgs(INCRBY, 'key', 1, { LABELS: { label: 'value' } }), ['TS.INCRBY', 'key', '1', 'LABELS', 'label', 'value'] @@ -67,7 +68,7 @@ describe('TS.INCRBY', () => { it ('with IGNORE', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1, { + parseArgs(INCRBY, 'key', 1, { IGNORE: { maxTimeDiff: 1, maxValDiff: 1 @@ -79,7 +80,7 @@ describe('TS.INCRBY', () => { it('with TIMESTAMP, RETENTION, UNCOMPRESSED, CHUNK_SIZE and LABELS', () => { assert.deepEqual( - INCRBY.transformArguments('key', 1, { + parseArgs(INCRBY, 'key', 1, { TIMESTAMP: '*', RETENTION: 1, UNCOMPRESSED: true, diff --git a/packages/time-series/lib/commands/INCRBY.ts b/packages/time-series/lib/commands/INCRBY.ts index 3160d3906d3..7dbdb6b9ead 100644 --- a/packages/time-series/lib/commands/INCRBY.ts +++ b/packages/time-series/lib/commands/INCRBY.ts @@ -1,5 +1,6 @@ -import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { Timestamp, transformTimestampArgument, pushRetentionArgument, pushChunkSizeArgument, Labels, pushLabelsArgument, pushIgnoreArgument } from '.'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { Timestamp, transformTimestampArgument, parseRetentionArgument, parseChunkSizeArgument, Labels, parseLabelsArgument, parseIgnoreArgument } from '.'; import { TsIgnoreOptions } from './ADD'; export interface TsIncrByOptions { @@ -11,40 +12,39 @@ export interface TsIncrByOptions { IGNORE?: TsIgnoreOptions; } -export function transformIncrByArguments( - command: RedisArgument, +export function parseIncrByArguments( + parser: CommandParser, key: RedisArgument, value: number, options?: TsIncrByOptions ) { - const args = [ - command, - key, - value.toString() - ]; + parser.pushKey(key); + parser.push(value.toString()); if (options?.TIMESTAMP !== undefined && options?.TIMESTAMP !== null) { - args.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP)); + parser.push('TIMESTAMP', transformTimestampArgument(options.TIMESTAMP)); } - pushRetentionArgument(args, options?.RETENTION); + parseRetentionArgument(parser, options?.RETENTION); if (options?.UNCOMPRESSED) { - args.push('UNCOMPRESSED'); + parser.push('UNCOMPRESSED'); } - pushChunkSizeArgument(args, options?.CHUNK_SIZE); + parseChunkSizeArgument(parser, options?.CHUNK_SIZE); - pushLabelsArgument(args, options?.LABELS); + parseLabelsArgument(parser, options?.LABELS); - pushIgnoreArgument(args, options?.IGNORE); - - return args; + parseIgnoreArgument(parser, options?.IGNORE); } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments: transformIncrByArguments.bind(undefined, 'TS.INCRBY'), + parseCommand(...args: Parameters) { + const parser = args[0]; + + parser.push('TS.INCRBY'); + parseIncrByArguments(...args); + }, transformReply: undefined as unknown as () => NumberReply } as const satisfies Command; diff --git a/packages/time-series/lib/commands/INFO.spec.ts b/packages/time-series/lib/commands/INFO.spec.ts index e4295b80fa4..73b9d8dc930 100644 --- a/packages/time-series/lib/commands/INFO.spec.ts +++ b/packages/time-series/lib/commands/INFO.spec.ts @@ -3,11 +3,12 @@ import { TIME_SERIES_DUPLICATE_POLICIES } from '.'; import testUtils, { GLOBAL } from '../test-utils'; import INFO, { InfoReply } from './INFO'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.INFO', () => { it('transformArguments', () => { assert.deepEqual( - INFO.transformArguments('key'), + parseArgs(INFO, 'key'), ['TS.INFO', 'key'] ); }); diff --git a/packages/time-series/lib/commands/INFO.ts b/packages/time-series/lib/commands/INFO.ts index 91d4e4bbad3..fbd66875b1f 100644 --- a/packages/time-series/lib/commands/INFO.ts +++ b/packages/time-series/lib/commands/INFO.ts @@ -1,3 +1,4 @@ +import { CommandParser } from '@redis/client/lib/client/parser'; import { ArrayReply, BlobStringReply, Command, DoubleReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/lib/RESP/types"; import { TimeSeriesDuplicatePolicies } from "."; import { TimeSeriesAggregationType } from "./CREATERULE"; @@ -71,10 +72,10 @@ export interface InfoReply { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments(key: string) { - return ['TS.INFO', key]; + parseCommand(parser: CommandParser, key: string) { + parser.push('TS.INFO'); + parser.pushKey(key); }, transformReply: { 2: (reply: InfoRawReply, _, typeMapping?: TypeMapping): InfoReply => { @@ -125,4 +126,4 @@ export default { 3: undefined as unknown as () => ReplyUnion }, unstableResp3: true - } as const satisfies Command; \ No newline at end of file + } as const satisfies Command; diff --git a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts index 674f91c60a7..063b9126550 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.spec.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.spec.ts @@ -4,11 +4,12 @@ import testUtils, { GLOBAL } from '../test-utils'; import { assertInfo } from './INFO.spec'; import INFO_DEBUG from './INFO_DEBUG'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.INFO_DEBUG', () => { it('transformArguments', () => { assert.deepEqual( - INFO_DEBUG.transformArguments('key'), + parseArgs(INFO_DEBUG, 'key'), ['TS.INFO', 'key', 'DEBUG'] ); }); diff --git a/packages/time-series/lib/commands/INFO_DEBUG.ts b/packages/time-series/lib/commands/INFO_DEBUG.ts index fb2b28b8072..bee1147f2bb 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.ts @@ -1,6 +1,6 @@ -import { BlobStringReply, Command, NumberReply, SimpleStringReply, TypeMapping } from "@redis/client/lib/RESP/types"; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { BlobStringReply, Command, NumberReply, SimpleStringReply, TypeMapping, ReplyUnion } from "@redis/client/lib/RESP/types"; import INFO, { InfoRawReply, InfoRawReplyTypes, InfoReply } from "./INFO"; -import { ReplyUnion } from '@redis/client/lib/RESP/types'; type chunkType = Array<[ 'startTimestamp', @@ -37,12 +37,10 @@ export interface InfoDebugReply extends InfoReply { } export default { - FIRST_KEY_INDEX: INFO.FIRST_KEY_INDEX, IS_READ_ONLY: INFO.IS_READ_ONLY, - transformArguments(key: string) { - const args = INFO.transformArguments(key); - args.push('DEBUG'); - return args; + parseCommand(parser: CommandParser, key: string) { + INFO.parseCommand(parser, key); + parser.push('DEBUG'); }, transformReply: { 2: (reply: InfoDebugRawReply, _, typeMapping?: TypeMapping): InfoDebugReply => { @@ -76,4 +74,4 @@ export default { 3: undefined as unknown as () => ReplyUnion }, unstableResp3: true -} as const satisfies Command; \ No newline at end of file +} as const satisfies Command; diff --git a/packages/time-series/lib/commands/MADD.spec.ts b/packages/time-series/lib/commands/MADD.spec.ts index bbe358e5438..8bf8e27fdb3 100644 --- a/packages/time-series/lib/commands/MADD.spec.ts +++ b/packages/time-series/lib/commands/MADD.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MADD from './MADD'; import { SimpleError } from '@redis/client/lib/errors'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MADD', () => { it('transformArguments', () => { assert.deepEqual( - MADD.transformArguments([{ + parseArgs(MADD, [{ key: '1', timestamp: 0, value: 0 diff --git a/packages/time-series/lib/commands/MADD.ts b/packages/time-series/lib/commands/MADD.ts index 59c1ed59bdb..cb1f077055a 100644 --- a/packages/time-series/lib/commands/MADD.ts +++ b/packages/time-series/lib/commands/MADD.ts @@ -1,5 +1,6 @@ +import { CommandParser } from '@redis/client/lib/client/parser'; import { Timestamp, transformTimestampArgument } from '.'; -import { ArrayReply, NumberReply, SimpleErrorReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { ArrayReply, NumberReply, SimpleErrorReply, Command } from '@redis/client/lib/RESP/types'; export interface TsMAddSample { key: string; @@ -8,20 +9,14 @@ export interface TsMAddSample { } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: false, - transformArguments(toAdd: Array) { - const args = ['TS.MADD']; + parseCommand(parser: CommandParser, toAdd: Array) { + parser.push('TS.MADD'); for (const { key, timestamp, value } of toAdd) { - args.push( - key, - transformTimestampArgument(timestamp), - value.toString() - ); + parser.pushKey(key); + parser.push(transformTimestampArgument(timestamp), value.toString()); } - - return args; }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MGET.spec.ts b/packages/time-series/lib/commands/MGET.spec.ts index b2de0486cfe..ba2e571be49 100644 --- a/packages/time-series/lib/commands/MGET.spec.ts +++ b/packages/time-series/lib/commands/MGET.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MGET from './MGET'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MGET', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( - MGET.transformArguments('label=value'), + parseArgs(MGET, 'label=value'), ['TS.MGET', 'FILTER', 'label=value'] ); }); it('with LATEST', () => { assert.deepEqual( - MGET.transformArguments('label=value', { + parseArgs(MGET, 'label=value', { LATEST: true }), ['TS.MGET', 'LATEST', 'FILTER', 'label=value'] diff --git a/packages/time-series/lib/commands/MGET.ts b/packages/time-series/lib/commands/MGET.ts index 2b04b29589b..add742a70d5 100644 --- a/packages/time-series/lib/commands/MGET.ts +++ b/packages/time-series/lib/commands/MGET.ts @@ -1,22 +1,21 @@ -import { CommandArguments, Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/lib/RESP/types'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, transformSampleReply } from '.'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export interface TsMGetOptions { LATEST?: boolean; } -export function pushLatestArgument(args: CommandArguments, latest?: boolean) { +export function parseLatestArgument(parser: CommandParser, latest?: boolean) { if (latest) { - args.push('LATEST'); + parser.push('LATEST'); } - - return args; } -export function pushFilterArgument(args: CommandArguments, filter: RedisVariadicArgument) { - args.push('FILTER'); - return pushVariadicArguments(args, filter); +export function parseFilterArgument(parser: CommandParser, filter: RedisVariadicArgument) { + parser.push('FILTER'); + parser.pushVariadic(filter); } export type MGetRawReply2 = ArrayReply< @@ -36,11 +35,12 @@ export type MGetRawReply3 = MapReply< >; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(filter: RedisVariadicArgument, options?: TsMGetOptions) { - const args = pushLatestArgument(['TS.MGET'], options?.LATEST); - return pushFilterArgument(args, filter); + parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetOptions) { + parser.push('TS.MGET'); + parseLatestArgument(parser, options?.LATEST); + parseFilterArgument(parser, filter); }, transformReply: { 2(reply: MGetRawReply2, _, typeMapping?: TypeMapping) { diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts index d9820027bb9..d79c463fc7d 100644 --- a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MGET_SELECTED_LABELS from './MGET_SELECTED_LABELS'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MGET_SELECTED_LABELS', () => { it('transformArguments', () => { assert.deepEqual( - MGET_SELECTED_LABELS.transformArguments('label=value', 'label'), + parseArgs(MGET_SELECTED_LABELS, 'label=value', 'label'), ['TS.MGET', 'SELECTED_LABELS', 'label', 'FILTER', 'label=value'] ); }); diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts index d132972d879..67c7dc79600 100644 --- a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts @@ -1,16 +1,17 @@ -import { Command, BlobStringReply, NullReply } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { TsMGetOptions, pushLatestArgument, pushFilterArgument } from './MGET'; -import { pushSelectedLabelsArguments } from '.'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, BlobStringReply, NullReply } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { TsMGetOptions, parseLatestArgument, parseFilterArgument } from './MGET'; +import { parseSelectedLabelsArguments } from '.'; import { createTransformMGetLabelsReply } from './MGET_WITHLABELS'; export default { - FIRST_KEY_INDEX: undefined, IS_READ_ONLY: true, - transformArguments(filter: RedisVariadicArgument, selectedLabels: RedisVariadicArgument, options?: TsMGetOptions) { - let args = pushLatestArgument(['TS.MGET'], options?.LATEST); - args = pushSelectedLabelsArguments(args, selectedLabels); - return pushFilterArgument(args, filter); + parseCommand(parser: CommandParser, filter: RedisVariadicArgument, selectedLabels: RedisVariadicArgument, options?: TsMGetOptions) { + parser.push('TS.MGET'); + parseLatestArgument(parser, options?.LATEST); + parseSelectedLabelsArguments(parser, selectedLabels); + parseFilterArgument(parser, filter); }, transformReply: createTransformMGetLabelsReply(), } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts index d3e51d2cab6..33fc5308444 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.spec.ts @@ -1,11 +1,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MGET_WITHLABELS from './MGET_WITHLABELS'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MGET_WITHLABELS', () => { it('transformArguments', () => { assert.deepEqual( - MGET_WITHLABELS.transformArguments('label=value'), + parseArgs(MGET_WITHLABELS, 'label=value'), ['TS.MGET', 'WITHLABELS', 'FILTER', 'label=value'] ); }); diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.ts index 679a536f2ab..f6d50c91b14 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.ts @@ -1,6 +1,7 @@ -import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { TsMGetOptions, pushLatestArgument, pushFilterArgument } from './MGET'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { TsMGetOptions, parseLatestArgument, parseFilterArgument } from './MGET'; import { RawLabelValue, resp2MapToValue, resp3MapToValue, SampleRawReply, transformRESP2Labels, transformSampleReply } from '.'; export interface TsMGetWithLabelsOptions extends TsMGetOptions { @@ -50,12 +51,12 @@ export function createTransformMGetLabelsReply() { } export default { - FIRST_KEY_INDEX: undefined, IS_READ_ONLY: true, - transformArguments(filter: RedisVariadicArgument, options?: TsMGetOptions) { - const args = pushLatestArgument(['TS.MGET'], options?.LATEST); - args.push('WITHLABELS'); - return pushFilterArgument(args, filter); + parseCommand(parser: CommandParser, filter: RedisVariadicArgument, options?: TsMGetWithLabelsOptions) { + parser.push('TS.MGET'); + parseLatestArgument(parser, options?.LATEST); + parser.push('WITHLABELS'); + parseFilterArgument(parser, filter); }, transformReply: createTransformMGetLabelsReply(), } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MRANGE.spec.ts b/packages/time-series/lib/commands/MRANGE.spec.ts index 9d41763eb02..94c8e72983a 100644 --- a/packages/time-series/lib/commands/MRANGE.spec.ts +++ b/packages/time-series/lib/commands/MRANGE.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MRANGE from './MRANGE'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MRANGE', () => { it('transformArguments', () => { assert.deepEqual( - MRANGE.transformArguments('-', '+', 'label=value', { + parseArgs(MRANGE, '-', '+', 'label=value', { LATEST: true, FILTER_BY_TS: [0], FILTER_BY_VALUE: { diff --git a/packages/time-series/lib/commands/MRANGE.ts b/packages/time-series/lib/commands/MRANGE.ts index bbc93a70dad..dbe48d6f542 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -1,8 +1,9 @@ -import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; -import { TsRangeOptions, pushRangeArguments } from './RANGE'; -import { pushFilterArgument } from './MGET'; +import { TsRangeOptions, parseRangeArguments } from './RANGE'; +import { parseFilterArgument } from './MGET'; export type TsMRangeRawReply2 = ArrayReply< TuplesReply<[ @@ -23,26 +24,28 @@ export type TsMRangeRawReply3 = MapReply< export function createTransformMRangeArguments(command: RedisArgument) { return ( + parser: CommandParser, fromTimestamp: Timestamp, toTimestamp: Timestamp, filter: RedisVariadicArgument, options?: TsRangeOptions ) => { - const args = pushRangeArguments( - [command], + parser.push(command); + parseRangeArguments( + parser, fromTimestamp, toTimestamp, options ); - return pushFilterArgument(args, filter); + parseFilterArgument(parser, filter); }; } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments: createTransformMRangeArguments('TS.MRANGE'), + parseCommand: createTransformMRangeArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeRawReply2, _?: any, typeMapping?: TypeMapping) { return resp2MapToValue(reply, ([_key, _labels, samples]) => { diff --git a/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts b/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts index c0d05425ff4..bcdde20fe98 100644 --- a/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MRANGE_GROUPBY, { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MRANGE_GROUPBY', () => { it('transformArguments', () => { assert.deepEqual( - MRANGE_GROUPBY.transformArguments('-', '+', 'label=value', { + parseArgs(MRANGE_GROUPBY, '-', '+', 'label=value', { REDUCE: TIME_SERIES_REDUCERS.AVG, label: 'label' }, { diff --git a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts index 3b4e94eac20..0d996521d1c 100644 --- a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts @@ -1,8 +1,9 @@ -import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument, TuplesToMapReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument, TuplesToMapReply, UnwrapReply } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; -import { TsRangeOptions, pushRangeArguments } from './RANGE'; -import { pushFilterArgument } from './MGET'; +import { TsRangeOptions, parseRangeArguments } from './RANGE'; +import { parseFilterArgument } from './MGET'; export const TIME_SERIES_REDUCERS = { AVG: 'AVG', @@ -24,8 +25,8 @@ export interface TsMRangeGroupBy { REDUCE: TimeSeriesReducer; } -export function pushGroupByArguments(args: Array, groupBy: TsMRangeGroupBy) { - args.push('GROUPBY', groupBy.label, 'REDUCE', groupBy.REDUCE); +export function parseGroupByArguments(parser: CommandParser, groupBy: TsMRangeGroupBy) { + parser.push('GROUPBY', groupBy.label, 'REDUCE', groupBy.REDUCE); } export type TsMRangeGroupByRawReply2 = ArrayReply< @@ -52,24 +53,19 @@ export type TsMRangeGroupByRawReply3 = MapReply< export function createTransformMRangeGroupByArguments(command: RedisArgument) { return ( + parser: CommandParser, fromTimestamp: Timestamp, toTimestamp: Timestamp, filter: RedisVariadicArgument, groupBy: TsMRangeGroupBy, options?: TsRangeOptions ) => { - let args = pushRangeArguments( - [command], - fromTimestamp, - toTimestamp, - options - ); - - args = pushFilterArgument(args, filter); - - pushGroupByArguments(args, groupBy); + parser.push(command); + parseRangeArguments(parser, fromTimestamp, toTimestamp, options) - return args; + parseFilterArgument(parser, filter); + + parseGroupByArguments(parser, groupBy); }; } @@ -85,9 +81,8 @@ export function extractResp3MRangeSources(raw: TsMRangeGroupByRawMetadataReply3) } export default { - FIRST_KEY_INDEX: undefined, IS_READ_ONLY: true, - transformArguments: createTransformMRangeGroupByArguments('TS.MRANGE'), + parseCommand: createTransformMRangeGroupByArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeGroupByRawReply2, _?: any, typeMapping?: TypeMapping) { return resp2MapToValue(reply, ([_key, _labels, samples]) => { diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts index 5c15bad89e8..92680dea375 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MRANGE_SELECTED_LABELS', () => { it('transformArguments', () => { assert.deepEqual( - MRANGE_SELECTED_LABELS.transformArguments('-', '+', 'label', 'label=value', { + parseArgs(MRANGE_SELECTED_LABELS, '-', '+', 'label', 'label=value', { FILTER_BY_TS: [0], FILTER_BY_VALUE: { min: 0, diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts index f91f9583330..115944a2f40 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts @@ -1,8 +1,9 @@ -import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, NullReply, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { pushSelectedLabelsArguments, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2Labels, transformSamplesReply } from '.'; -import { TsRangeOptions, pushRangeArguments } from './RANGE'; -import { pushFilterArgument } from './MGET'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, NullReply, RedisArgument } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { parseSelectedLabelsArguments, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2Labels, transformSamplesReply } from '.'; +import { TsRangeOptions, parseRangeArguments } from './RANGE'; +import { parseFilterArgument } from './MGET'; export type TsMRangeSelectedLabelsRawReply2 = ArrayReply< TuplesReply<[ @@ -26,29 +27,30 @@ export type TsMRangeSelectedLabelsRawReply3 = MapReply< export function createTransformMRangeSelectedLabelsArguments(command: RedisArgument) { return ( + parser: CommandParser, fromTimestamp: Timestamp, toTimestamp: Timestamp, selectedLabels: RedisVariadicArgument, filter: RedisVariadicArgument, options?: TsRangeOptions ) => { - let args = pushRangeArguments( - [command], + parser.push(command); + parseRangeArguments( + parser, fromTimestamp, toTimestamp, options ); - args = pushSelectedLabelsArguments(args, selectedLabels); + parseSelectedLabelsArguments(parser, selectedLabels); - return pushFilterArgument(args, filter); + parseFilterArgument(parser, filter); }; } export default { - FIRST_KEY_INDEX: undefined, IS_READ_ONLY: true, - transformArguments: createTransformMRangeSelectedLabelsArguments('TS.MRANGE'), + parseCommand: createTransformMRangeSelectedLabelsArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeSelectedLabelsRawReply2, _?: any, typeMapping?: TypeMapping) { return resp2MapToValue(reply, ([_key, labels, samples]) => { diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts index 90090a851aa..4e5b2b47094 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.spec.ts @@ -3,11 +3,12 @@ import testUtils, { GLOBAL } from '../test-utils'; import MRANGE_SELECTED_LABELS_GROUPBY from './MRANGE_SELECTED_LABELS_GROUPBY'; import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MRANGE_SELECTED_LABELS_GROUPBY', () => { it('transformArguments', () => { assert.deepEqual( - MRANGE_SELECTED_LABELS_GROUPBY.transformArguments('-', '+', 'label', 'label=value', { + parseArgs(MRANGE_SELECTED_LABELS_GROUPBY, '-', '+', 'label', 'label=value', { REDUCE: TIME_SERIES_REDUCERS.AVG, label: 'label' }, { diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts index 7a798c41137..b4e8006a84e 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts @@ -1,9 +1,10 @@ -import { Command, ArrayReply, BlobStringReply, MapReply, TuplesReply, RedisArgument, NullReply } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; -import { pushSelectedLabelsArguments, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; -import { TsRangeOptions, pushRangeArguments } from './RANGE'; -import { extractResp3MRangeSources, pushGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; -import { pushFilterArgument } from './MGET'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, MapReply, TuplesReply, RedisArgument, NullReply } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { parseSelectedLabelsArguments, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; +import { TsRangeOptions, parseRangeArguments } from './RANGE'; +import { extractResp3MRangeSources, parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; +import { parseFilterArgument } from './MGET'; import MRANGE_SELECTED_LABELS from './MRANGE_SELECTED_LABELS'; export type TsMRangeWithLabelsGroupByRawReply3 = MapReply< @@ -20,6 +21,7 @@ export function createMRangeSelectedLabelsGroupByTransformArguments( command: RedisArgument ) { return ( + parser: CommandParser, fromTimestamp: Timestamp, toTimestamp: Timestamp, selectedLabels: RedisVariadicArgument, @@ -27,27 +29,25 @@ export function createMRangeSelectedLabelsGroupByTransformArguments( groupBy: TsMRangeGroupBy, options?: TsRangeOptions ) => { - let args = pushRangeArguments( - [command], + parser.push(command); + parseRangeArguments( + parser, fromTimestamp, toTimestamp, options ); - args = pushSelectedLabelsArguments(args, selectedLabels); + parseSelectedLabelsArguments(parser, selectedLabels); - args = pushFilterArgument(args, filter); + parseFilterArgument(parser, filter); - pushGroupByArguments(args, groupBy); - - return args; + parseGroupByArguments(parser, groupBy); }; } export default { - FIRST_KEY_INDEX: undefined, IS_READ_ONLY: true, - transformArguments: createMRangeSelectedLabelsGroupByTransformArguments('TS.MRANGE'), + parseCommand: createMRangeSelectedLabelsGroupByTransformArguments('TS.MRANGE'), transformReply: { 2: MRANGE_SELECTED_LABELS.transformReply[2], 3(reply: TsMRangeWithLabelsGroupByRawReply3) { diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts index fabf04b60dc..eab2e1fadbe 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MRANGE_WITHLABELS from './MRANGE_WITHLABELS'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MRANGE_WITHLABELS', () => { it('transformArguments', () => { assert.deepEqual( - MRANGE_WITHLABELS.transformArguments('-', '+', 'label=value', { + parseArgs(MRANGE_WITHLABELS, '-', '+', 'label=value', { LATEST: true, FILTER_BY_TS: [0], FILTER_BY_VALUE: { diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts index ab7a4ec8f6a..04d72411b7e 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts @@ -1,8 +1,9 @@ -import { Command, UnwrapReply, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, UnwrapReply, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; -import { TsRangeOptions, pushRangeArguments } from './RANGE'; -import { pushFilterArgument } from './MGET'; +import { TsRangeOptions, parseRangeArguments } from './RANGE'; +import { parseFilterArgument } from './MGET'; export type TsMRangeWithLabelsRawReply2 = ArrayReply< TuplesReply<[ @@ -26,28 +27,30 @@ export type TsMRangeWithLabelsRawReply3 = MapReply< export function createTransformMRangeWithLabelsArguments(command: RedisArgument) { return ( + parser: CommandParser, fromTimestamp: Timestamp, toTimestamp: Timestamp, filter: RedisVariadicArgument, options?: TsRangeOptions ) => { - const args = pushRangeArguments( - [command], + parser.push(command); + parseRangeArguments( + parser, fromTimestamp, toTimestamp, options ); - args.push('WITHLABELS'); + parser.push('WITHLABELS'); - return pushFilterArgument(args, filter); + parseFilterArgument(parser, filter); }; } export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments: createTransformMRangeWithLabelsArguments('TS.MRANGE'), + parseCommand: createTransformMRangeWithLabelsArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeWithLabelsRawReply2, _?: any, typeMapping?: TypeMapping) { return resp2MapToValue(reply, ([_key, labels, samples]) => { diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts index 755c3aca320..4a8b8fe707f 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.spec.ts @@ -3,11 +3,12 @@ import testUtils, { GLOBAL } from '../test-utils'; import MRANGE_WITHLABELS_GROUPBY from './MRANGE_WITHLABELS_GROUPBY'; import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MRANGE_WITHLABELS_GROUPBY', () => { it('transformArguments', () => { assert.deepEqual( - MRANGE_WITHLABELS_GROUPBY.transformArguments('-', '+', 'label=value', { + parseArgs(MRANGE_WITHLABELS_GROUPBY, '-', '+', 'label=value', { label: 'label', REDUCE: TIME_SERIES_REDUCERS.AVG }, { diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts index 7c5e0af368b..f09d630dcd4 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts @@ -1,9 +1,10 @@ -import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2LabelsWithSources, transformSamplesReply } from '.'; -import { TsRangeOptions, pushRangeArguments } from './RANGE'; -import { extractResp3MRangeSources, pushGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; -import { pushFilterArgument } from './MGET'; +import { TsRangeOptions, parseRangeArguments } from './RANGE'; +import { extractResp3MRangeSources, parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; +import { parseFilterArgument } from './MGET'; export type TsMRangeWithLabelsGroupByRawReply2 = ArrayReply< TuplesReply<[ @@ -28,33 +29,32 @@ export type TsMRangeWithLabelsGroupByRawReply3 = MapReply< export function createMRangeWithLabelsGroupByTransformArguments(command: RedisArgument) { return ( + parser: CommandParser, fromTimestamp: Timestamp, toTimestamp: Timestamp, filter: RedisVariadicArgument, groupBy: TsMRangeGroupBy, options?: TsRangeOptions ) => { - let args = pushRangeArguments( - [command], + parser.push(command); + parseRangeArguments( + parser, fromTimestamp, toTimestamp, options ); - args.push('WITHLABELS'); + parser.push('WITHLABELS'); - args = pushFilterArgument(args, filter); + parseFilterArgument(parser, filter); - pushGroupByArguments(args, groupBy); - - return args; - }; + parseGroupByArguments(parser, groupBy); + }; } export default { - FIRST_KEY_INDEX: undefined, IS_READ_ONLY: true, - transformArguments: createMRangeWithLabelsGroupByTransformArguments('TS.MRANGE'), + parseCommand: createMRangeWithLabelsGroupByTransformArguments('TS.MRANGE'), transformReply: { 2(reply: TsMRangeWithLabelsGroupByRawReply2, _?: any, typeMapping?: TypeMapping) { return resp2MapToValue(reply, ([_key, labels, samples]) => { diff --git a/packages/time-series/lib/commands/MREVRANGE.spec.ts b/packages/time-series/lib/commands/MREVRANGE.spec.ts index 8d6b8d3c148..09051103f8b 100644 --- a/packages/time-series/lib/commands/MREVRANGE.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MREVRANGE from './MREVRANGE'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MREVRANGE', () => { it('transformArguments', () => { assert.deepEqual( - MREVRANGE.transformArguments('-', '+', 'label=value', { + parseArgs(MREVRANGE, '-', '+', 'label=value', { LATEST: true, FILTER_BY_TS: [0], FILTER_BY_VALUE: { diff --git a/packages/time-series/lib/commands/MREVRANGE.ts b/packages/time-series/lib/commands/MREVRANGE.ts index 097176e6832..e2ed6d9cc91 100644 --- a/packages/time-series/lib/commands/MREVRANGE.ts +++ b/packages/time-series/lib/commands/MREVRANGE.ts @@ -1,9 +1,9 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; +import { Command } from '@redis/client/lib/RESP/types'; import MRANGE, { createTransformMRangeArguments } from './MRANGE'; export default { - FIRST_KEY_INDEX: MRANGE.FIRST_KEY_INDEX, + NOT_KEYED_COMMAND: MRANGE.NOT_KEYED_COMMAND, IS_READ_ONLY: MRANGE.IS_READ_ONLY, - transformArguments: createTransformMRangeArguments('TS.MREVRANGE'), + parseCommand: createTransformMRangeArguments('TS.MREVRANGE'), transformReply: MRANGE.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts index 9ccebc6c517..d32d675ad0a 100644 --- a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.spec.ts @@ -3,11 +3,12 @@ import testUtils, { GLOBAL } from '../test-utils'; import MREVRANGE_GROUPBY from './MREVRANGE_GROUPBY'; import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MREVRANGE_GROUPBY', () => { it('transformArguments', () => { assert.deepEqual( - MREVRANGE_GROUPBY.transformArguments('-', '+', 'label=value', { + parseArgs(MREVRANGE_GROUPBY, '-', '+', 'label=value', { REDUCE: TIME_SERIES_REDUCERS.AVG, label: 'label' }, { diff --git a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts index 24b2e6142f6..efdd1acdcfe 100644 --- a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts @@ -1,9 +1,8 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; +import { Command } from '@redis/client/lib/RESP/types'; import MRANGE_GROUPBY, { createTransformMRangeGroupByArguments } from './MRANGE_GROUPBY'; export default { - FIRST_KEY_INDEX: MRANGE_GROUPBY.FIRST_KEY_INDEX, IS_READ_ONLY: MRANGE_GROUPBY.IS_READ_ONLY, - transformArguments: createTransformMRangeGroupByArguments('TS.MREVRANGE'), + parseCommand: createTransformMRangeGroupByArguments('TS.MREVRANGE'), transformReply: MRANGE_GROUPBY.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts index f0533010b84..f68e34727c2 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MREVRANGE_SELECTED_LABELS from './MREVRANGE_SELECTED_LABELS'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MREVRANGE_SELECTED_LABELS', () => { it('transformArguments', () => { assert.deepEqual( - MREVRANGE_SELECTED_LABELS.transformArguments('-', '+', 'label', 'label=value', { + parseArgs(MREVRANGE_SELECTED_LABELS, '-', '+', 'label', 'label=value', { FILTER_BY_TS: [0], FILTER_BY_VALUE: { min: 0, diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts index 8656b768c28..8b679e65b6a 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts @@ -1,9 +1,8 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; +import { Command } from '@redis/client/lib/RESP/types'; import MRANGE_SELECTED_LABELS, { createTransformMRangeSelectedLabelsArguments } from './MRANGE_SELECTED_LABELS'; export default { - FIRST_KEY_INDEX: MRANGE_SELECTED_LABELS.FIRST_KEY_INDEX, IS_READ_ONLY: MRANGE_SELECTED_LABELS.IS_READ_ONLY, - transformArguments: createTransformMRangeSelectedLabelsArguments('TS.MREVRANGE'), + parseCommand: createTransformMRangeSelectedLabelsArguments('TS.MREVRANGE'), transformReply: MRANGE_SELECTED_LABELS.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts index 34ef4ff79a0..444bb2f3d24 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.spec.ts @@ -3,11 +3,12 @@ import testUtils, { GLOBAL } from '../test-utils'; import MREVRANGE_SELECTED_LABELS_GROUPBY from './MREVRANGE_SELECTED_LABELS_GROUPBY'; import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MREVRANGE_SELECTED_LABELS_GROUPBY', () => { it('transformArguments', () => { assert.deepEqual( - MREVRANGE_SELECTED_LABELS_GROUPBY.transformArguments('-', '+', 'label', 'label=value', { + parseArgs(MREVRANGE_SELECTED_LABELS_GROUPBY, '-', '+', 'label', 'label=value', { REDUCE: TIME_SERIES_REDUCERS.AVG, label: 'label' }, { diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts index f47330367b7..d01ebe1033a 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts @@ -1,9 +1,8 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; +import { Command } from '@redis/client/lib/RESP/types'; import MRANGE_SELECTED_LABELS_GROUPBY, { createMRangeSelectedLabelsGroupByTransformArguments } from './MRANGE_SELECTED_LABELS_GROUPBY'; export default { - FIRST_KEY_INDEX: MRANGE_SELECTED_LABELS_GROUPBY.FIRST_KEY_INDEX, IS_READ_ONLY: MRANGE_SELECTED_LABELS_GROUPBY.IS_READ_ONLY, - transformArguments: createMRangeSelectedLabelsGroupByTransformArguments('TS.MREVRANGE'), + parseCommand: createMRangeSelectedLabelsGroupByTransformArguments('TS.MREVRANGE'), transformReply: MRANGE_SELECTED_LABELS_GROUPBY.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts index eb88f233e43..da43a715f2e 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import MREVRANGE_WITHLABELS from './MREVRANGE_WITHLABELS'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MREVRANGE_WITHLABELS', () => { it('transformArguments', () => { assert.deepEqual( - MREVRANGE_WITHLABELS.transformArguments('-', '+', 'label=value', { + parseArgs(MREVRANGE_WITHLABELS, '-', '+', 'label=value', { LATEST: true, FILTER_BY_TS: [0], FILTER_BY_VALUE: { diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts index 81356d845fd..d4f6255592e 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts @@ -1,9 +1,9 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; +import { Command } from '@redis/client/lib/RESP/types'; import MRANGE_WITHLABELS, { createTransformMRangeWithLabelsArguments } from './MRANGE_WITHLABELS'; export default { - FIRST_KEY_INDEX: MRANGE_WITHLABELS.FIRST_KEY_INDEX, + NOT_KEYED_COMMAND: MRANGE_WITHLABELS.NOT_KEYED_COMMAND, IS_READ_ONLY: MRANGE_WITHLABELS.IS_READ_ONLY, - transformArguments: createTransformMRangeWithLabelsArguments('TS.MREVRANGE'), + parseCommand: createTransformMRangeWithLabelsArguments('TS.MREVRANGE'), transformReply: MRANGE_WITHLABELS.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts index da2c358b330..f4e6df9f0c6 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.spec.ts @@ -3,11 +3,12 @@ import testUtils, { GLOBAL } from '../test-utils'; import MREVRANGE_WITHLABELS_GROUPBY from './MREVRANGE_WITHLABELS_GROUPBY'; import { TIME_SERIES_REDUCERS } from './MRANGE_GROUPBY'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.MREVRANGE_WITHLABELS_GROUPBY', () => { it('transformArguments', () => { assert.deepEqual( - MREVRANGE_WITHLABELS_GROUPBY.transformArguments('-', '+', 'label=value', { + parseArgs(MREVRANGE_WITHLABELS_GROUPBY, '-', '+', 'label=value', { label: 'label', REDUCE: TIME_SERIES_REDUCERS.AVG }, { diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts index b3d49643fd0..ed43d0eae6b 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts @@ -1,9 +1,8 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; +import { Command } from '@redis/client/lib/RESP/types'; import MRANGE_WITHLABELS_GROUPBY, { createMRangeWithLabelsGroupByTransformArguments } from './MRANGE_WITHLABELS_GROUPBY'; export default { - FIRST_KEY_INDEX: MRANGE_WITHLABELS_GROUPBY.FIRST_KEY_INDEX, IS_READ_ONLY: MRANGE_WITHLABELS_GROUPBY.IS_READ_ONLY, - transformArguments: createMRangeWithLabelsGroupByTransformArguments('TS.MREVRANGE'), + parseCommand: createMRangeWithLabelsGroupByTransformArguments('TS.MREVRANGE'), transformReply: MRANGE_WITHLABELS_GROUPBY.transformReply, } as const satisfies Command; diff --git a/packages/time-series/lib/commands/QUERYINDEX.spec.ts b/packages/time-series/lib/commands/QUERYINDEX.spec.ts index 74f201bb74b..2f3f5617fb3 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.spec.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.spec.ts @@ -1,19 +1,20 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import QUERYINDEX from './QUERYINDEX'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.QUERYINDEX', () => { describe('transformArguments', () => { it('single filter', () => { assert.deepEqual( - QUERYINDEX.transformArguments('*'), + parseArgs(QUERYINDEX, '*'), ['TS.QUERYINDEX', '*'] ); }); it('multiple filters', () => { assert.deepEqual( - QUERYINDEX.transformArguments(['a=1', 'b=2']), + parseArgs(QUERYINDEX, ['a=1', 'b=2']), ['TS.QUERYINDEX', 'a=1', 'b=2'] ); }); diff --git a/packages/time-series/lib/commands/QUERYINDEX.ts b/packages/time-series/lib/commands/QUERYINDEX.ts index 86c2a3c5a7e..69625801974 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.ts @@ -1,11 +1,13 @@ -import { ArrayReply, BlobStringReply, SetReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { ArrayReply, BlobStringReply, SetReply, Command } from '@redis/client/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; export default { - FIRST_KEY_INDEX: undefined, + NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - transformArguments(filter: RedisVariadicArgument) { - return pushVariadicArguments(['TS.QUERYINDEX'], filter); + parseCommand(parser: CommandParser, filter: RedisVariadicArgument) { + parser.push('TS.QUERYINDEX'); + parser.pushVariadic(filter); }, transformReply: { 2: undefined as unknown as () => ArrayReply, diff --git a/packages/time-series/lib/commands/RANGE.spec.ts b/packages/time-series/lib/commands/RANGE.spec.ts index bc5d38d740b..2d20b455fc1 100644 --- a/packages/time-series/lib/commands/RANGE.spec.ts +++ b/packages/time-series/lib/commands/RANGE.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import RANGE from './RANGE'; import { TIME_SERIES_AGGREGATION_TYPE } from './CREATERULE'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.RANGE', () => { it('transformArguments', () => { assert.deepEqual( - RANGE.transformArguments('key', '-', '+', { + parseArgs(RANGE, 'key', '-', '+', { FILTER_BY_TS: [0], FILTER_BY_VALUE: { min: 1, diff --git a/packages/time-series/lib/commands/RANGE.ts b/packages/time-series/lib/commands/RANGE.ts index 084073fefe6..ef4c79fe603 100644 --- a/packages/time-series/lib/commands/RANGE.ts +++ b/packages/time-series/lib/commands/RANGE.ts @@ -1,7 +1,8 @@ -import { CommandArguments, RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; import { Timestamp, transformTimestampArgument, SamplesRawReply, transformSamplesReply } from '.'; import { TimeSeriesAggregationType } from './CREATERULE'; -import { Resp2Reply } from '@redis/client/dist/lib/RESP/types'; +import { Resp2Reply } from '@redis/client/lib/RESP/types'; export const TIME_SERIES_BUCKET_TIMESTAMP = { LOW: '-', @@ -29,30 +30,30 @@ export interface TsRangeOptions { }; } -export function pushRangeArguments( - args: CommandArguments, +export function parseRangeArguments( + parser: CommandParser, fromTimestamp: Timestamp, toTimestamp: Timestamp, options?: TsRangeOptions ) { - args.push( + parser.push( transformTimestampArgument(fromTimestamp), transformTimestampArgument(toTimestamp) ); if (options?.LATEST) { - args.push('LATEST'); + parser.push('LATEST'); } if (options?.FILTER_BY_TS) { - args.push('FILTER_BY_TS'); + parser.push('FILTER_BY_TS'); for (const timestamp of options.FILTER_BY_TS) { - args.push(transformTimestampArgument(timestamp)); + parser.push(transformTimestampArgument(timestamp)); } } if (options?.FILTER_BY_VALUE) { - args.push( + parser.push( 'FILTER_BY_VALUE', options.FILTER_BY_VALUE.min.toString(), options.FILTER_BY_VALUE.max.toString() @@ -60,54 +61,52 @@ export function pushRangeArguments( } if (options?.COUNT !== undefined) { - args.push('COUNT', options.COUNT.toString()); + parser.push('COUNT', options.COUNT.toString()); } if (options?.AGGREGATION) { if (options?.ALIGN !== undefined) { - args.push('ALIGN', transformTimestampArgument(options.ALIGN)); + parser.push('ALIGN', transformTimestampArgument(options.ALIGN)); } - args.push( + parser.push( 'AGGREGATION', options.AGGREGATION.type, transformTimestampArgument(options.AGGREGATION.timeBucket) ); if (options.AGGREGATION.BUCKETTIMESTAMP) { - args.push( + parser.push( 'BUCKETTIMESTAMP', options.AGGREGATION.BUCKETTIMESTAMP ); } if (options.AGGREGATION.EMPTY) { - args.push('EMPTY'); + parser.push('EMPTY'); } } - - return args; } export function transformRangeArguments( - command: RedisArgument, + parser: CommandParser, key: RedisArgument, fromTimestamp: Timestamp, toTimestamp: Timestamp, options?: TsRangeOptions ) { - return pushRangeArguments( - [command, key], - fromTimestamp, - toTimestamp, - options - ); + parser.pushKey(key); + parseRangeArguments(parser, fromTimestamp, toTimestamp, options); } export default { - FIRST_KEY_INDEX: 1, IS_READ_ONLY: true, - transformArguments: transformRangeArguments.bind(undefined, 'TS.RANGE'), + parseCommand(...args: Parameters) { + const parser = args[0]; + + parser.push('TS.RANGE'); + transformRangeArguments(...args); + }, transformReply: { 2(reply: Resp2Reply) { return transformSamplesReply[2](reply); diff --git a/packages/time-series/lib/commands/REVRANGE.spec.ts b/packages/time-series/lib/commands/REVRANGE.spec.ts index c371e8306b0..a4c6aa2c0db 100644 --- a/packages/time-series/lib/commands/REVRANGE.spec.ts +++ b/packages/time-series/lib/commands/REVRANGE.spec.ts @@ -2,11 +2,12 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import REVRANGE from './REVRANGE'; import { TIME_SERIES_AGGREGATION_TYPE } from '../index'; +import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('TS.REVRANGE', () => { it('transformArguments', () => { assert.deepEqual( - REVRANGE.transformArguments('key', '-', '+', { + parseArgs(REVRANGE, 'key', '-', '+', { FILTER_BY_TS: [0], FILTER_BY_VALUE: { min: 1, diff --git a/packages/time-series/lib/commands/REVRANGE.ts b/packages/time-series/lib/commands/REVRANGE.ts index 1097223080b..24a31a785a2 100644 --- a/packages/time-series/lib/commands/REVRANGE.ts +++ b/packages/time-series/lib/commands/REVRANGE.ts @@ -1,9 +1,13 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; +import { Command } from '@redis/client/lib/RESP/types'; import RANGE, { transformRangeArguments } from './RANGE'; export default { - FIRST_KEY_INDEX: RANGE.FIRST_KEY_INDEX, IS_READ_ONLY: RANGE.IS_READ_ONLY, - transformArguments: transformRangeArguments.bind(undefined, 'TS.REVRANGE'), + parseCommand(...args: Parameters) { + const parser = args[0]; + + parser.push('TS.REVRANGE'); + transformRangeArguments(...args); + }, transformReply: RANGE.transformReply } as const satisfies Command; diff --git a/packages/time-series/lib/commands/index.spec.ts b/packages/time-series/lib/commands/index.spec.ts index ff7f4afad68..5b28708152f 100644 --- a/packages/time-series/lib/commands/index.spec.ts +++ b/packages/time-series/lib/commands/index.spec.ts @@ -1,4 +1,4 @@ -// import { RedisCommandArguments } from '@redis/client/dist/lib/commands'; +// import { RedisCommandArguments } from '@redis/client/lib/commands'; // import { strict as assert } from 'node:assert'; // import { // transformTimestampArgument, diff --git a/packages/time-series/lib/commands/index.ts b/packages/time-series/lib/commands/index.ts index 5b9d97b6566..e0389a60a2e 100644 --- a/packages/time-series/lib/commands/index.ts +++ b/packages/time-series/lib/commands/index.ts @@ -1,4 +1,4 @@ -import type { DoubleReply, NumberReply, RedisArgument, RedisCommands, TuplesReply, UnwrapReply, Resp2Reply, ArrayReply, BlobStringReply, MapReply, NullReply, TypeMapping, ReplyUnion, RespType } from '@redis/client/lib/RESP/types'; +import type { DoubleReply, NumberReply, RedisCommands, TuplesReply, UnwrapReply, Resp2Reply, ArrayReply, BlobStringReply, MapReply, NullReply, TypeMapping, ReplyUnion, RespType } from '@redis/client/lib/RESP/types'; import ADD, { TsIgnoreOptions } from './ADD'; import ALTER from './ALTER'; import CREATE from './CREATE'; @@ -29,7 +29,8 @@ import MREVRANGE from './MREVRANGE'; import QUERYINDEX from './QUERYINDEX'; import RANGE from './RANGE'; import REVRANGE from './REVRANGE'; -import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/lib/commands/generic-transformers'; +import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/lib/client/parser'; import { RESP_TYPES } from '@redis/client/lib/RESP/decoder'; export default { @@ -95,15 +96,15 @@ export default { revRange: REVRANGE } as const satisfies RedisCommands; -export function pushIgnoreArgument(args: Array, ignore?: TsIgnoreOptions) { +export function parseIgnoreArgument(parser: CommandParser, ignore?: TsIgnoreOptions) { if (ignore !== undefined) { - args.push('IGNORE', ignore.maxTimeDiff.toString(), ignore.maxValDiff.toString()); + parser.push('IGNORE', ignore.maxTimeDiff.toString(), ignore.maxValDiff.toString()); } } -export function pushRetentionArgument(args: Array, retention?: number) { +export function parseRetentionArgument(parser: CommandParser, retention?: number) { if (retention !== undefined) { - args.push('RETENTION', retention.toString()); + parser.push('RETENTION', retention.toString()); } } @@ -114,15 +115,15 @@ export const TIME_SERIES_ENCODING = { export type TimeSeriesEncoding = typeof TIME_SERIES_ENCODING[keyof typeof TIME_SERIES_ENCODING]; -export function pushEncodingArgument(args: Array, encoding?: TimeSeriesEncoding) { +export function parseEncodingArgument(parser: CommandParser, encoding?: TimeSeriesEncoding) { if (encoding !== undefined) { - args.push('ENCODING', encoding); + parser.push('ENCODING', encoding); } } -export function pushChunkSizeArgument(args: Array, chunkSize?: number) { +export function parseChunkSizeArgument(parser: CommandParser, chunkSize?: number) { if (chunkSize !== undefined) { - args.push('CHUNK_SIZE', chunkSize.toString()); + parser.push('CHUNK_SIZE', chunkSize.toString()); } } @@ -137,9 +138,9 @@ export const TIME_SERIES_DUPLICATE_POLICIES = { export type TimeSeriesDuplicatePolicies = typeof TIME_SERIES_DUPLICATE_POLICIES[keyof typeof TIME_SERIES_DUPLICATE_POLICIES]; -export function pushDuplicatePolicy(args: Array, duplicatePolicy?: TimeSeriesDuplicatePolicies) { +export function parseDuplicatePolicy(parser: CommandParser, duplicatePolicy?: TimeSeriesDuplicatePolicies) { if (duplicatePolicy !== undefined) { - args.push('DUPLICATE_POLICY', duplicatePolicy); + parser.push('DUPLICATE_POLICY', duplicatePolicy); } } @@ -159,16 +160,14 @@ export type Labels = { [label: string]: string; }; -export function pushLabelsArgument(args: Array, labels?: Labels) { +export function parseLabelsArgument(parser: CommandParser, labels?: Labels) { if (labels) { - args.push('LABELS'); + parser.push('LABELS'); for (const [label, value] of Object.entries(labels)) { - args.push(label, value); + parser.push(label, value); } } - - return args; } export type SampleRawReply = TuplesReply<[timestamp: NumberReply, value: DoubleReply]>; @@ -269,12 +268,12 @@ export function resp3MapToValue< return reply as never; } -export function pushSelectedLabelsArguments( - args: Array, +export function parseSelectedLabelsArguments( + parser: CommandParser, selectedLabels: RedisVariadicArgument ) { - args.push('SELECTED_LABELS'); - return pushVariadicArguments(args, selectedLabels); + parser.push('SELECTED_LABELS'); + parser.pushVariadic(selectedLabels); } export type RawLabelValue = BlobStringReply | NullReply; From 8dab27ed02cc4b52c60ed3e069f4a3df1a1d0a2e Mon Sep 17 00:00:00 2001 From: Shaya Potter Date: Mon, 4 Nov 2024 17:23:34 +0200 Subject: [PATCH 017/105] fix sentinel generics (#2859) * fix sentinel generics * comment nit --- packages/client/lib/sentinel/index.ts | 12 ++++++------ packages/client/lib/sentinel/types.ts | 6 ++++-- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index d25fa03e559..92a87fbb145 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -93,10 +93,10 @@ export class RedisSentinelClient< RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} >( + options: RedisSentinelOptions, internal: RedisSentinelInternal, clientInfo: ClientInfo, commandOptions?: CommandOptions, - options?: RedisSentinelOptions ) { return RedisSentinelClient.factory(options)(internal, clientInfo, commandOptions); } @@ -272,7 +272,7 @@ export default class RedisSentinel< this.#options = options; - if (options?.commandOptions) { + if (options.commandOptions) { this.#commandOptions = options.commandOptions; } @@ -307,7 +307,7 @@ export default class RedisSentinel< Sentinel.prototype.Multi = RedisSentinelMultiCommand.extend(config); - return (options?: Omit>) => { + return (options: Omit>) => { // returning a "proxy" to prevent the namespaces.self to leak between "proxies" return Object.create(new Sentinel(options)) as RedisSentinelType; }; @@ -319,7 +319,7 @@ export default class RedisSentinel< S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} - >(options?: RedisSentinelOptions) { + >(options: RedisSentinelOptions) { return RedisSentinel.factory(options)(options); } @@ -409,7 +409,7 @@ export default class RedisSentinel< try { return await fn( - RedisSentinelClient.create(this._self.#internal, clientInfo, this._self.#commandOptions, this._self.#options) + RedisSentinelClient.create(this._self.#options, this._self.#internal, clientInfo, this._self.#commandOptions) ); } finally { const promise = this._self.#internal.releaseClientLease(clientInfo); @@ -510,7 +510,7 @@ export default class RedisSentinel< async aquire(): Promise> { const clientInfo = await this._self.#internal.getClientLease(); - return RedisSentinelClient.create(this._self.#internal, clientInfo, this._self.#commandOptions, this._self.#options); + return RedisSentinelClient.create(this._self.#options, this._self.#internal, clientInfo, this._self.#commandOptions); } getSentinelNode(): RedisNode | undefined { diff --git a/packages/client/lib/sentinel/types.ts b/packages/client/lib/sentinel/types.ts index 1f868ec5177..428e7bccd66 100644 --- a/packages/client/lib/sentinel/types.ts +++ b/packages/client/lib/sentinel/types.ts @@ -29,14 +29,16 @@ export interface RedisSentinelOptions< * The maximum number of times a command will retry due to topology changes. */ maxCommandRediscovers?: number; + // TODO: omit properties that users shouldn't be able to specify for sentinel at this level /** * The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with */ - nodeClientOptions?: RedisClientOptions; + nodeClientOptions?: RedisClientOptions; + // TODO: omit properties that users shouldn't be able to specify for sentinel at this level /** * The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with */ - sentinelClientOptions?: RedisClientOptions; + sentinelClientOptions?: RedisClientOptions; /** * The number of clients connected to the master node */ From b835309cf8f2cc27c3543956a45bb170e0672124 Mon Sep 17 00:00:00 2001 From: Leibale Eidelman Date: Mon, 4 Nov 2024 12:21:17 -0500 Subject: [PATCH 018/105] fix cross packages imports --- packages/bloom/lib/commands/bloom/ADD.ts | 6 +++--- packages/bloom/lib/commands/bloom/CARD.ts | 4 ++-- packages/bloom/lib/commands/bloom/EXISTS.ts | 6 +++--- packages/bloom/lib/commands/bloom/INFO.ts | 4 ++-- packages/bloom/lib/commands/bloom/INSERT.ts | 8 ++++---- packages/bloom/lib/commands/bloom/LOADCHUNK.ts | 4 ++-- packages/bloom/lib/commands/bloom/MADD.ts | 8 ++++---- packages/bloom/lib/commands/bloom/MEXISTS.ts | 8 ++++---- packages/bloom/lib/commands/bloom/RESERVE.ts | 4 ++-- packages/bloom/lib/commands/bloom/SCANDUMP.ts | 4 ++-- packages/bloom/lib/commands/bloom/index.ts | 2 +- packages/bloom/lib/commands/count-min-sketch/INCRBY.ts | 4 ++-- packages/bloom/lib/commands/count-min-sketch/INFO.ts | 4 ++-- packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts | 4 ++-- .../bloom/lib/commands/count-min-sketch/INITBYPROB.ts | 4 ++-- packages/bloom/lib/commands/count-min-sketch/MERGE.ts | 4 ++-- packages/bloom/lib/commands/count-min-sketch/QUERY.ts | 6 +++--- packages/bloom/lib/commands/count-min-sketch/index.ts | 2 +- packages/bloom/lib/commands/cuckoo/ADD.ts | 6 +++--- packages/bloom/lib/commands/cuckoo/ADDNX.ts | 6 +++--- packages/bloom/lib/commands/cuckoo/COUNT.ts | 4 ++-- packages/bloom/lib/commands/cuckoo/DEL.ts | 6 +++--- packages/bloom/lib/commands/cuckoo/EXISTS.ts | 6 +++--- packages/bloom/lib/commands/cuckoo/INFO.ts | 4 ++-- packages/bloom/lib/commands/cuckoo/INSERT.ts | 6 +++--- packages/bloom/lib/commands/cuckoo/INSERTNX.ts | 2 +- packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts | 4 ++-- packages/bloom/lib/commands/cuckoo/RESERVE.ts | 4 ++-- packages/bloom/lib/commands/cuckoo/SCANDUMP.ts | 4 ++-- packages/bloom/lib/commands/cuckoo/index.ts | 2 +- packages/bloom/lib/commands/t-digest/ADD.ts | 4 ++-- packages/bloom/lib/commands/t-digest/BYRANK.ts | 6 +++--- packages/bloom/lib/commands/t-digest/BYREVRANK.ts | 2 +- packages/bloom/lib/commands/t-digest/CDF.ts | 6 +++--- packages/bloom/lib/commands/t-digest/CREATE.ts | 4 ++-- packages/bloom/lib/commands/t-digest/INFO.ts | 4 ++-- packages/bloom/lib/commands/t-digest/MAX.ts | 6 +++--- packages/bloom/lib/commands/t-digest/MERGE.ts | 6 +++--- packages/bloom/lib/commands/t-digest/MIN.ts | 6 +++--- packages/bloom/lib/commands/t-digest/QUANTILE.ts | 6 +++--- packages/bloom/lib/commands/t-digest/RANK.ts | 4 ++-- packages/bloom/lib/commands/t-digest/RESET.ts | 4 ++-- packages/bloom/lib/commands/t-digest/REVRANK.ts | 2 +- packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts | 6 +++--- packages/bloom/lib/commands/t-digest/index.ts | 2 +- packages/bloom/lib/commands/top-k/ADD.ts | 6 +++--- packages/bloom/lib/commands/top-k/COUNT.ts | 6 +++--- packages/bloom/lib/commands/top-k/INCRBY.ts | 4 ++-- packages/bloom/lib/commands/top-k/INFO.ts | 6 +++--- packages/bloom/lib/commands/top-k/LIST.ts | 4 ++-- packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts | 4 ++-- packages/bloom/lib/commands/top-k/QUERY.ts | 6 +++--- packages/bloom/lib/commands/top-k/RESERVE.ts | 4 ++-- packages/bloom/lib/commands/top-k/index.ts | 2 +- packages/graph/lib/commands/CONFIG_GET.ts | 4 ++-- packages/graph/lib/commands/CONFIG_SET.ts | 4 ++-- packages/graph/lib/commands/DELETE.ts | 4 ++-- packages/graph/lib/commands/EXPLAIN.ts | 4 ++-- packages/graph/lib/commands/LIST.ts | 4 ++-- packages/graph/lib/commands/PROFILE.ts | 4 ++-- packages/graph/lib/commands/QUERY.ts | 4 ++-- packages/graph/lib/commands/RO_QUERY.ts | 2 +- packages/graph/lib/commands/SLOWLOG.ts | 4 ++-- packages/graph/lib/commands/index.ts | 2 +- packages/graph/lib/graph.ts | 2 +- packages/json/lib/commands/ARRAPPEND.ts | 4 ++-- packages/json/lib/commands/ARRINDEX.ts | 4 ++-- packages/json/lib/commands/ARRINSERT.ts | 4 ++-- packages/json/lib/commands/ARRLEN.ts | 4 ++-- packages/json/lib/commands/ARRPOP.ts | 6 +++--- packages/json/lib/commands/ARRTRIM.ts | 4 ++-- packages/json/lib/commands/CLEAR.ts | 4 ++-- packages/json/lib/commands/DEBUG_MEMORY.ts | 4 ++-- packages/json/lib/commands/DEL.ts | 4 ++-- packages/json/lib/commands/FORGET.ts | 4 ++-- packages/json/lib/commands/GET.ts | 6 +++--- packages/json/lib/commands/MERGE.ts | 4 ++-- packages/json/lib/commands/MGET.ts | 4 ++-- packages/json/lib/commands/MSET.ts | 4 ++-- packages/json/lib/commands/NUMINCRBY.ts | 4 ++-- packages/json/lib/commands/NUMMULTBY.ts | 4 ++-- packages/json/lib/commands/OBJKEYS.ts | 4 ++-- packages/json/lib/commands/OBJLEN.ts | 4 ++-- packages/json/lib/commands/RESP.ts | 4 ++-- packages/json/lib/commands/SET.ts | 4 ++-- packages/json/lib/commands/STRAPPEND.ts | 4 ++-- packages/json/lib/commands/STRLEN.ts | 4 ++-- packages/json/lib/commands/TOGGLE.ts | 4 ++-- packages/json/lib/commands/TYPE.ts | 4 ++-- packages/json/lib/commands/index.ts | 4 ++-- packages/search/lib/commands/AGGREGATE.ts | 6 +++--- packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts | 4 ++-- packages/search/lib/commands/ALIASADD.ts | 4 ++-- packages/search/lib/commands/ALIASDEL.ts | 4 ++-- packages/search/lib/commands/ALIASUPDATE.ts | 4 ++-- packages/search/lib/commands/ALTER.ts | 4 ++-- packages/search/lib/commands/CONFIG_GET.ts | 4 ++-- packages/search/lib/commands/CONFIG_SET.ts | 4 ++-- packages/search/lib/commands/CREATE.ts | 6 +++--- packages/search/lib/commands/CURSOR_DEL.ts | 4 ++-- packages/search/lib/commands/CURSOR_READ.ts | 4 ++-- packages/search/lib/commands/DICTADD.ts | 6 +++--- packages/search/lib/commands/DICTDEL.ts | 6 +++--- packages/search/lib/commands/DICTDUMP.ts | 4 ++-- packages/search/lib/commands/DROPINDEX.ts | 4 ++-- packages/search/lib/commands/EXPLAIN.ts | 4 ++-- packages/search/lib/commands/EXPLAINCLI.ts | 4 ++-- packages/search/lib/commands/INFO.ts | 8 ++++---- packages/search/lib/commands/PROFILE_AGGREGATE.ts | 4 ++-- packages/search/lib/commands/PROFILE_SEARCH.ts | 4 ++-- packages/search/lib/commands/SEARCH.ts | 6 +++--- packages/search/lib/commands/SEARCH_NOCONTENT.ts | 2 +- packages/search/lib/commands/SPELLCHECK.ts | 4 ++-- packages/search/lib/commands/SUGADD.ts | 4 ++-- packages/search/lib/commands/SUGDEL.ts | 4 ++-- packages/search/lib/commands/SUGGET.ts | 4 ++-- packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts | 4 ++-- packages/search/lib/commands/SUGGET_WITHSCORES.ts | 4 ++-- .../search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts | 4 ++-- packages/search/lib/commands/SUGLEN.ts | 4 ++-- packages/search/lib/commands/SYNDUMP.ts | 4 ++-- packages/search/lib/commands/SYNUPDATE.ts | 6 +++--- packages/search/lib/commands/TAGVALS.ts | 4 ++-- packages/search/lib/commands/_LIST.ts | 4 ++-- packages/test-utils/lib/dockers.ts | 2 +- packages/time-series/lib/commands/ADD.ts | 4 ++-- packages/time-series/lib/commands/ALTER.ts | 4 ++-- packages/time-series/lib/commands/CREATE.ts | 4 ++-- packages/time-series/lib/commands/CREATERULE.ts | 4 ++-- packages/time-series/lib/commands/DECRBY.ts | 2 +- packages/time-series/lib/commands/DEL.ts | 4 ++-- packages/time-series/lib/commands/DELETERULE.ts | 4 ++-- packages/time-series/lib/commands/GET.ts | 4 ++-- packages/time-series/lib/commands/INCRBY.ts | 4 ++-- packages/time-series/lib/commands/INFO.ts | 6 +++--- packages/time-series/lib/commands/INFO_DEBUG.ts | 4 ++-- packages/time-series/lib/commands/MADD.ts | 4 ++-- packages/time-series/lib/commands/MGET.ts | 6 +++--- packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts | 6 +++--- packages/time-series/lib/commands/MGET_WITHLABELS.ts | 6 +++--- packages/time-series/lib/commands/MRANGE.ts | 6 +++--- packages/time-series/lib/commands/MRANGE_GROUPBY.ts | 6 +++--- .../time-series/lib/commands/MRANGE_SELECTED_LABELS.ts | 6 +++--- .../lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts | 6 +++--- packages/time-series/lib/commands/MRANGE_WITHLABELS.ts | 6 +++--- .../time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts | 6 +++--- packages/time-series/lib/commands/MREVRANGE.ts | 2 +- packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts | 2 +- .../time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts | 2 +- .../lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts | 2 +- packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts | 2 +- .../lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts | 2 +- packages/time-series/lib/commands/QUERYINDEX.ts | 6 +++--- packages/time-series/lib/commands/RANGE.ts | 6 +++--- packages/time-series/lib/commands/REVRANGE.ts | 2 +- packages/time-series/lib/commands/index.ts | 8 ++++---- 156 files changed, 340 insertions(+), 340 deletions(-) diff --git a/packages/bloom/lib/commands/bloom/ADD.ts b/packages/bloom/lib/commands/bloom/ADD.ts index f394755acc4..e12d9cfa1d2 100644 --- a/packages/bloom/lib/commands/bloom/ADD.ts +++ b/packages/bloom/lib/commands/bloom/ADD.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/bloom/CARD.ts b/packages/bloom/lib/commands/bloom/CARD.ts index 1d206b30af9..c2f9aeb00fc 100644 --- a/packages/bloom/lib/commands/bloom/CARD.ts +++ b/packages/bloom/lib/commands/bloom/CARD.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/bloom/EXISTS.ts b/packages/bloom/lib/commands/bloom/EXISTS.ts index 3de18dd07cd..b3f19af9516 100644 --- a/packages/bloom/lib/commands/bloom/EXISTS.ts +++ b/packages/bloom/lib/commands/bloom/EXISTS.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/bloom/INFO.ts b/packages/bloom/lib/commands/bloom/INFO.ts index b1e3f137c8c..b715bf57738 100644 --- a/packages/bloom/lib/commands/bloom/INFO.ts +++ b/packages/bloom/lib/commands/bloom/INFO.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command, UnwrapReply, NullReply, NumberReply, TuplesToMapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command, UnwrapReply, NullReply, NumberReply, TuplesToMapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; import { transformInfoV2Reply } from '.'; export type BfInfoReplyMap = TuplesToMapReply<[ diff --git a/packages/bloom/lib/commands/bloom/INSERT.ts b/packages/bloom/lib/commands/bloom/INSERT.ts index 14831f2f11a..b8dcef325f8 100644 --- a/packages/bloom/lib/commands/bloom/INSERT.ts +++ b/packages/bloom/lib/commands/bloom/INSERT.ts @@ -1,7 +1,7 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; -import { transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export interface BfInsertOptions { CAPACITY?: number; diff --git a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts index 47036e042af..ef3cc4a3e12 100644 --- a/packages/bloom/lib/commands/bloom/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/bloom/LOADCHUNK.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/bloom/MADD.ts b/packages/bloom/lib/commands/bloom/MADD.ts index fda7419bf19..efbd932b403 100644 --- a/packages/bloom/lib/commands/bloom/MADD.ts +++ b/packages/bloom/lib/commands/bloom/MADD.ts @@ -1,7 +1,7 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; -import { transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/bloom/MEXISTS.ts b/packages/bloom/lib/commands/bloom/MEXISTS.ts index acd85786f97..a5a311a8e4c 100644 --- a/packages/bloom/lib/commands/bloom/MEXISTS.ts +++ b/packages/bloom/lib/commands/bloom/MEXISTS.ts @@ -1,7 +1,7 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; -import { transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/bloom/RESERVE.ts b/packages/bloom/lib/commands/bloom/RESERVE.ts index aff155ab769..00f17c1889f 100644 --- a/packages/bloom/lib/commands/bloom/RESERVE.ts +++ b/packages/bloom/lib/commands/bloom/RESERVE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface BfReserveOptions { EXPANSION?: number; diff --git a/packages/bloom/lib/commands/bloom/SCANDUMP.ts b/packages/bloom/lib/commands/bloom/SCANDUMP.ts index 37b1f57045c..d0472b649c5 100644 --- a/packages/bloom/lib/commands/bloom/SCANDUMP.ts +++ b/packages/bloom/lib/commands/bloom/SCANDUMP.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/bloom/index.ts b/packages/bloom/lib/commands/bloom/index.ts index e87f57220dd..a93f79c9c56 100644 --- a/packages/bloom/lib/commands/bloom/index.ts +++ b/packages/bloom/lib/commands/bloom/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands, TypeMapping } from '@redis/client/lib/RESP/types'; +import type { RedisCommands, TypeMapping } from '@redis/client/dist/lib/RESP/types'; import ADD from './ADD'; import CARD from './CARD'; diff --git a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts index 39cc52d31de..a011957ece6 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INCRBY.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface BfIncrByItem { item: RedisArgument; diff --git a/packages/bloom/lib/commands/count-min-sketch/INFO.ts b/packages/bloom/lib/commands/count-min-sketch/INFO.ts index 9b77409f2d1..fef1cac97e7 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INFO.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INFO.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, TuplesToMapReply, NumberReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, TuplesToMapReply, NumberReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; import { transformInfoV2Reply } from '../bloom'; export type CmsInfoReplyMap = TuplesToMapReply<[ diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts index cd295d0696e..44e6a75952f 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYDIM.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts index e7e85d4100b..3b96120bd04 100644 --- a/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts +++ b/packages/bloom/lib/commands/count-min-sketch/INITBYPROB.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts index cc0fc929070..4d959bd619d 100644 --- a/packages/bloom/lib/commands/count-min-sketch/MERGE.ts +++ b/packages/bloom/lib/commands/count-min-sketch/MERGE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; interface BfMergeSketch { name: RedisArgument; diff --git a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts index 54d838c1935..b55b51d1bbd 100644 --- a/packages/bloom/lib/commands/count-min-sketch/QUERY.ts +++ b/packages/bloom/lib/commands/count-min-sketch/QUERY.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { ArrayReply, NumberReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { ArrayReply, NumberReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/count-min-sketch/index.ts b/packages/bloom/lib/commands/count-min-sketch/index.ts index 1132a7524e1..4f0f395ca3d 100644 --- a/packages/bloom/lib/commands/count-min-sketch/index.ts +++ b/packages/bloom/lib/commands/count-min-sketch/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands } from '@redis/client/lib/RESP/types'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; import INCRBY from './INCRBY'; import INFO from './INFO'; import INITBYDIM from './INITBYDIM'; diff --git a/packages/bloom/lib/commands/cuckoo/ADD.ts b/packages/bloom/lib/commands/cuckoo/ADD.ts index 3d0dc77be2d..37a5d1b5b86 100644 --- a/packages/bloom/lib/commands/cuckoo/ADD.ts +++ b/packages/bloom/lib/commands/cuckoo/ADD.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/cuckoo/ADDNX.ts b/packages/bloom/lib/commands/cuckoo/ADDNX.ts index f358f1581ec..ceaf62be21c 100644 --- a/packages/bloom/lib/commands/cuckoo/ADDNX.ts +++ b/packages/bloom/lib/commands/cuckoo/ADDNX.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/cuckoo/COUNT.ts b/packages/bloom/lib/commands/cuckoo/COUNT.ts index 512b0143272..f0cd5a72105 100644 --- a/packages/bloom/lib/commands/cuckoo/COUNT.ts +++ b/packages/bloom/lib/commands/cuckoo/COUNT.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/cuckoo/DEL.ts b/packages/bloom/lib/commands/cuckoo/DEL.ts index 0b2bdaea990..c97b7c2d9fc 100644 --- a/packages/bloom/lib/commands/cuckoo/DEL.ts +++ b/packages/bloom/lib/commands/cuckoo/DEL.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/cuckoo/EXISTS.ts b/packages/bloom/lib/commands/cuckoo/EXISTS.ts index ef93462990b..2299cb3de99 100644 --- a/packages/bloom/lib/commands/cuckoo/EXISTS.ts +++ b/packages/bloom/lib/commands/cuckoo/EXISTS.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformBooleanReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformBooleanReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/cuckoo/INFO.ts b/packages/bloom/lib/commands/cuckoo/INFO.ts index 15972b206ff..6a8f06f1e77 100644 --- a/packages/bloom/lib/commands/cuckoo/INFO.ts +++ b/packages/bloom/lib/commands/cuckoo/INFO.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; import { transformInfoV2Reply } from '../bloom'; export type CfInfoReplyMap = TuplesToMapReply<[ diff --git a/packages/bloom/lib/commands/cuckoo/INSERT.ts b/packages/bloom/lib/commands/cuckoo/INSERT.ts index 75534e0a7fa..3ad3feee16d 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERT.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERT.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, RedisArgument } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export interface CfInsertOptions { CAPACITY?: number; diff --git a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts index 581cfcd9e60..7ddc952e2fa 100644 --- a/packages/bloom/lib/commands/cuckoo/INSERTNX.ts +++ b/packages/bloom/lib/commands/cuckoo/INSERTNX.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import INSERT, { parseCfInsertArguments } from './INSERT'; export default { diff --git a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts index 420774a6504..8fb21be8e0d 100644 --- a/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts +++ b/packages/bloom/lib/commands/cuckoo/LOADCHUNK.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/cuckoo/RESERVE.ts b/packages/bloom/lib/commands/cuckoo/RESERVE.ts index ba401dcdee9..2685b0db06d 100644 --- a/packages/bloom/lib/commands/cuckoo/RESERVE.ts +++ b/packages/bloom/lib/commands/cuckoo/RESERVE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface CfReserveOptions { BUCKETSIZE?: number; diff --git a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts index aab7a5eecc2..25ef2c3f6da 100644 --- a/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts +++ b/packages/bloom/lib/commands/cuckoo/SCANDUMP.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, TuplesReply, NumberReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/cuckoo/index.ts b/packages/bloom/lib/commands/cuckoo/index.ts index 7ee99b41cd3..62c63fe8d19 100644 --- a/packages/bloom/lib/commands/cuckoo/index.ts +++ b/packages/bloom/lib/commands/cuckoo/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands } from '@redis/client/lib/RESP/types'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; import ADD from './ADD'; import ADDNX from './ADDNX'; import COUNT from './COUNT'; diff --git a/packages/bloom/lib/commands/t-digest/ADD.ts b/packages/bloom/lib/commands/t-digest/ADD.ts index d9d67144a95..5534d58065b 100644 --- a/packages/bloom/lib/commands/t-digest/ADD.ts +++ b/packages/bloom/lib/commands/t-digest/ADD.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/t-digest/BYRANK.ts b/packages/bloom/lib/commands/t-digest/BYRANK.ts index 126c9963e71..9c1ab0059f3 100644 --- a/packages/bloom/lib/commands/t-digest/BYRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYRANK.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformDoubleArrayReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export function transformByRankArguments( parser: CommandParser, diff --git a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts index 4995bf86cc0..8721c081e7a 100644 --- a/packages/bloom/lib/commands/t-digest/BYREVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/BYREVRANK.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import BYRANK, { transformByRankArguments } from './BYRANK'; export default { diff --git a/packages/bloom/lib/commands/t-digest/CDF.ts b/packages/bloom/lib/commands/t-digest/CDF.ts index e786dc4c8a8..4d1d8ea2786 100644 --- a/packages/bloom/lib/commands/t-digest/CDF.ts +++ b/packages/bloom/lib/commands/t-digest/CDF.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformDoubleArrayReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/t-digest/CREATE.ts b/packages/bloom/lib/commands/t-digest/CREATE.ts index fd160cce842..58b1e008284 100644 --- a/packages/bloom/lib/commands/t-digest/CREATE.ts +++ b/packages/bloom/lib/commands/t-digest/CREATE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface TDigestCreateOptions { COMPRESSION?: number; diff --git a/packages/bloom/lib/commands/t-digest/INFO.ts b/packages/bloom/lib/commands/t-digest/INFO.ts index 43624e8f9b9..2cb9e93443c 100644 --- a/packages/bloom/lib/commands/t-digest/INFO.ts +++ b/packages/bloom/lib/commands/t-digest/INFO.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command, NumberReply, TuplesToMapReply, UnwrapReply, Resp2Reply, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; import { transformInfoV2Reply } from '../bloom'; export type TdInfoReplyMap = TuplesToMapReply<[ diff --git a/packages/bloom/lib/commands/t-digest/MAX.ts b/packages/bloom/lib/commands/t-digest/MAX.ts index 64852d8e6dc..140db6a3e48 100644 --- a/packages/bloom/lib/commands/t-digest/MAX.ts +++ b/packages/bloom/lib/commands/t-digest/MAX.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/t-digest/MERGE.ts b/packages/bloom/lib/commands/t-digest/MERGE.ts index 1031f0e9170..80049d1e540 100644 --- a/packages/bloom/lib/commands/t-digest/MERGE.ts +++ b/packages/bloom/lib/commands/t-digest/MERGE.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface TDigestMergeOptions { COMPRESSION?: number; diff --git a/packages/bloom/lib/commands/t-digest/MIN.ts b/packages/bloom/lib/commands/t-digest/MIN.ts index cc44dbbea4e..d6e56fb672e 100644 --- a/packages/bloom/lib/commands/t-digest/MIN.ts +++ b/packages/bloom/lib/commands/t-digest/MIN.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/t-digest/QUANTILE.ts b/packages/bloom/lib/commands/t-digest/QUANTILE.ts index 962c3a9b4ca..1c27b5f6ec6 100644 --- a/packages/bloom/lib/commands/t-digest/QUANTILE.ts +++ b/packages/bloom/lib/commands/t-digest/QUANTILE.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformDoubleArrayReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/t-digest/RANK.ts b/packages/bloom/lib/commands/t-digest/RANK.ts index 316ca74b542..053c0c544e9 100644 --- a/packages/bloom/lib/commands/t-digest/RANK.ts +++ b/packages/bloom/lib/commands/t-digest/RANK.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; export function transformRankArguments( parser: CommandParser, diff --git a/packages/bloom/lib/commands/t-digest/RESET.ts b/packages/bloom/lib/commands/t-digest/RESET.ts index 571102bfc20..c2bda72d6d4 100644 --- a/packages/bloom/lib/commands/t-digest/RESET.ts +++ b/packages/bloom/lib/commands/t-digest/RESET.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/t-digest/REVRANK.ts b/packages/bloom/lib/commands/t-digest/REVRANK.ts index ca9301bb423..e7f2357a2f8 100644 --- a/packages/bloom/lib/commands/t-digest/REVRANK.ts +++ b/packages/bloom/lib/commands/t-digest/REVRANK.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import RANK, { transformRankArguments } from './RANK'; export default { diff --git a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts index f411198260a..1fd6360ab65 100644 --- a/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts +++ b/packages/bloom/lib/commands/t-digest/TRIMMED_MEAN.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/t-digest/index.ts b/packages/bloom/lib/commands/t-digest/index.ts index fb80b35d0a1..d180911dbf9 100644 --- a/packages/bloom/lib/commands/t-digest/index.ts +++ b/packages/bloom/lib/commands/t-digest/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands } from '@redis/client/lib/RESP/types'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; import ADD from './ADD'; import BYRANK from './BYRANK'; import BYREVRANK from './BYREVRANK'; diff --git a/packages/bloom/lib/commands/top-k/ADD.ts b/packages/bloom/lib/commands/top-k/ADD.ts index 42b162165c7..244e6209c91 100644 --- a/packages/bloom/lib/commands/top-k/ADD.ts +++ b/packages/bloom/lib/commands/top-k/ADD.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/top-k/COUNT.ts b/packages/bloom/lib/commands/top-k/COUNT.ts index 7cca009c599..7e75a3b68aa 100644 --- a/packages/bloom/lib/commands/top-k/COUNT.ts +++ b/packages/bloom/lib/commands/top-k/COUNT.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/top-k/INCRBY.ts b/packages/bloom/lib/commands/top-k/INCRBY.ts index 52ea0edc968..9e9a49e18f9 100644 --- a/packages/bloom/lib/commands/top-k/INCRBY.ts +++ b/packages/bloom/lib/commands/top-k/INCRBY.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, SimpleStringReply, NullReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface TopKIncrByItem { item: string; diff --git a/packages/bloom/lib/commands/top-k/INFO.ts b/packages/bloom/lib/commands/top-k/INFO.ts index 89ebeaa8ebe..53da265c1f2 100644 --- a/packages/bloom/lib/commands/top-k/INFO.ts +++ b/packages/bloom/lib/commands/top-k/INFO.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, TuplesToMapReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/lib/RESP/types'; -import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, TuplesToMapReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command, SimpleStringReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; import { transformInfoV2Reply } from '../bloom'; export type TopKInfoReplyMap = TuplesToMapReply<[ diff --git a/packages/bloom/lib/commands/top-k/LIST.ts b/packages/bloom/lib/commands/top-k/LIST.ts index 74b85e0f71d..d7adeaa193c 100644 --- a/packages/bloom/lib/commands/top-k/LIST.ts +++ b/packages/bloom/lib/commands/top-k/LIST.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts index a3a3d3f43f8..2c0f10e785b 100644 --- a/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts +++ b/packages/bloom/lib/commands/top-k/LIST_WITHCOUNT.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: true, diff --git a/packages/bloom/lib/commands/top-k/QUERY.ts b/packages/bloom/lib/commands/top-k/QUERY.ts index 49b91714374..a6fb4bae69e 100644 --- a/packages/bloom/lib/commands/top-k/QUERY.ts +++ b/packages/bloom/lib/commands/top-k/QUERY.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, transformBooleanArrayReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { IS_READ_ONLY: false, diff --git a/packages/bloom/lib/commands/top-k/RESERVE.ts b/packages/bloom/lib/commands/top-k/RESERVE.ts index f3ef3bf7200..ee3ee9a8cf4 100644 --- a/packages/bloom/lib/commands/top-k/RESERVE.ts +++ b/packages/bloom/lib/commands/top-k/RESERVE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; export interface TopKReserveOptions { width: number; diff --git a/packages/bloom/lib/commands/top-k/index.ts b/packages/bloom/lib/commands/top-k/index.ts index 0352745fd07..fb5de543cab 100644 --- a/packages/bloom/lib/commands/top-k/index.ts +++ b/packages/bloom/lib/commands/top-k/index.ts @@ -1,4 +1,4 @@ -import type { RedisCommands } from '@redis/client/lib/RESP/types'; +import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; import ADD from './ADD'; import COUNT from './COUNT'; import INCRBY from './INCRBY'; diff --git a/packages/graph/lib/commands/CONFIG_GET.ts b/packages/graph/lib/commands/CONFIG_GET.ts index 8ff289876d3..4986dfc1816 100644 --- a/packages/graph/lib/commands/CONFIG_GET.ts +++ b/packages/graph/lib/commands/CONFIG_GET.ts @@ -1,5 +1,5 @@ -import { RedisArgument, TuplesReply, ArrayReply, BlobStringReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; -import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, TuplesReply, ArrayReply, BlobStringReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; type ConfigItemReply = TuplesReply<[ configKey: BlobStringReply, diff --git a/packages/graph/lib/commands/CONFIG_SET.ts b/packages/graph/lib/commands/CONFIG_SET.ts index b37d8690bfa..63f604402ec 100644 --- a/packages/graph/lib/commands/CONFIG_SET.ts +++ b/packages/graph/lib/commands/CONFIG_SET.ts @@ -1,5 +1,5 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; -import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/graph/lib/commands/DELETE.ts b/packages/graph/lib/commands/DELETE.ts index 2e1f9ff003c..43af38d6fb0 100644 --- a/packages/graph/lib/commands/DELETE.ts +++ b/packages/graph/lib/commands/DELETE.ts @@ -1,5 +1,5 @@ -import { RedisArgument, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; -import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; export default { IS_READ_ONLY: false, diff --git a/packages/graph/lib/commands/EXPLAIN.ts b/packages/graph/lib/commands/EXPLAIN.ts index c690450a10f..a9af7e73a20 100644 --- a/packages/graph/lib/commands/EXPLAIN.ts +++ b/packages/graph/lib/commands/EXPLAIN.ts @@ -1,5 +1,5 @@ -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; -import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; export default { IS_READ_ONLY: true, diff --git a/packages/graph/lib/commands/LIST.ts b/packages/graph/lib/commands/LIST.ts index 4fe66d748fa..70fa8bda812 100644 --- a/packages/graph/lib/commands/LIST.ts +++ b/packages/graph/lib/commands/LIST.ts @@ -1,5 +1,5 @@ -import { ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; -import { CommandParser } from '@redis/client/lib/client/parser'; +import { ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/graph/lib/commands/PROFILE.ts b/packages/graph/lib/commands/PROFILE.ts index fba0973baaf..c3229fb9a04 100644 --- a/packages/graph/lib/commands/PROFILE.ts +++ b/packages/graph/lib/commands/PROFILE.ts @@ -1,5 +1,5 @@ -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; -import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; export default { IS_READ_ONLY: true, diff --git a/packages/graph/lib/commands/QUERY.ts b/packages/graph/lib/commands/QUERY.ts index c96c00ff325..23ea24c5768 100644 --- a/packages/graph/lib/commands/QUERY.ts +++ b/packages/graph/lib/commands/QUERY.ts @@ -1,5 +1,5 @@ -import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, NullReply, TuplesReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; -import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, NullReply, TuplesReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; type Headers = ArrayReply; diff --git a/packages/graph/lib/commands/RO_QUERY.ts b/packages/graph/lib/commands/RO_QUERY.ts index 98668675d21..f052877b99d 100644 --- a/packages/graph/lib/commands/RO_QUERY.ts +++ b/packages/graph/lib/commands/RO_QUERY.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import QUERY, { parseQueryArguments } from './QUERY'; export default { diff --git a/packages/graph/lib/commands/SLOWLOG.ts b/packages/graph/lib/commands/SLOWLOG.ts index bba615efb21..4110335f922 100644 --- a/packages/graph/lib/commands/SLOWLOG.ts +++ b/packages/graph/lib/commands/SLOWLOG.ts @@ -1,5 +1,5 @@ -import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; -import { CommandParser } from '@redis/client/lib/client/parser'; +import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; type SlowLogRawReply = ArrayReply; diff --git a/packages/json/lib/commands/SET.ts b/packages/json/lib/commands/SET.ts index 6bd89803ded..75d7099acfb 100644 --- a/packages/json/lib/commands/SET.ts +++ b/packages/json/lib/commands/SET.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, NullReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; import { RedisJSON, transformRedisJsonArgument } from '.'; export interface JsonSetOptions { diff --git a/packages/json/lib/commands/STRAPPEND.ts b/packages/json/lib/commands/STRAPPEND.ts index 97bbebf931b..45d503856ac 100644 --- a/packages/json/lib/commands/STRAPPEND.ts +++ b/packages/json/lib/commands/STRAPPEND.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command, NullReply, NumberReply, ArrayReply } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command, NullReply, NumberReply, ArrayReply } from '@redis/client/dist/lib/RESP/types'; import { transformRedisJsonArgument } from '.'; export interface JsonStrAppendOptions { diff --git a/packages/json/lib/commands/STRLEN.ts b/packages/json/lib/commands/STRLEN.ts index b72f30cd6da..644cdf27ef7 100644 --- a/packages/json/lib/commands/STRLEN.ts +++ b/packages/json/lib/commands/STRLEN.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface JsonStrLenOptions { path?: RedisArgument; diff --git a/packages/json/lib/commands/TOGGLE.ts b/packages/json/lib/commands/TOGGLE.ts index 2707d54b6fd..85c769729c7 100644 --- a/packages/json/lib/commands/TOGGLE.ts +++ b/packages/json/lib/commands/TOGGLE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, NumberReply, NullReply, Command, } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, NumberReply, NullReply, Command, } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, diff --git a/packages/json/lib/commands/TYPE.ts b/packages/json/lib/commands/TYPE.ts index d19de31df68..1146043b2c2 100644 --- a/packages/json/lib/commands/TYPE.ts +++ b/packages/json/lib/commands/TYPE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { NullReply, BlobStringReply, ArrayReply, Command, RedisArgument, UnwrapReply } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { NullReply, BlobStringReply, ArrayReply, Command, RedisArgument, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; export interface JsonTypeOptions { path?: RedisArgument; diff --git a/packages/json/lib/commands/index.ts b/packages/json/lib/commands/index.ts index 8ea44ce8049..2724ff2565c 100644 --- a/packages/json/lib/commands/index.ts +++ b/packages/json/lib/commands/index.ts @@ -1,4 +1,4 @@ -import { BlobStringReply, NullReply, UnwrapReply } from '@redis/client/lib/RESP/types'; +import { BlobStringReply, NullReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; import ARRAPPEND from './ARRAPPEND'; import ARRINDEX from './ARRINDEX'; import ARRINSERT from './ARRINSERT'; @@ -23,7 +23,7 @@ import STRAPPEND from './STRAPPEND'; import STRLEN from './STRLEN'; import TOGGLE from './TOGGLE'; import TYPE from './TYPE'; -import { isNullReply } from '@redis/client/lib/commands/generic-transformers'; +import { isNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; export default { ARRAPPEND, diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts index 0105b082682..925a91da008 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -1,8 +1,8 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { ArrayReply, BlobStringReply, Command, MapReply, NumberReply, RedisArgument, ReplyUnion, TypeMapping, UnwrapReply } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { ArrayReply, BlobStringReply, Command, MapReply, NumberReply, RedisArgument, ReplyUnion, TypeMapping, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; import { RediSearchProperty } from './CREATE'; import { FtSearchParams, parseParamsArgument } from './SEARCH'; -import { transformTuplesReply } from '@redis/client/lib/commands/generic-transformers'; +import { transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers'; type LoadField = RediSearchProperty | { identifier: RediSearchProperty; diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts index f9a7e75942f..8dfca7169ef 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command, ReplyUnion, NumberReply } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command, ReplyUnion, NumberReply } from '@redis/client/dist/lib/RESP/types'; import AGGREGATE, { AggregateRawReply, AggregateReply, FtAggregateOptions } from './AGGREGATE'; export interface FtAggregateWithCursorOptions extends FtAggregateOptions { diff --git a/packages/search/lib/commands/ALIASADD.ts b/packages/search/lib/commands/ALIASADD.ts index db8eb54326e..c35e60bed4f 100644 --- a/packages/search/lib/commands/ALIASADD.ts +++ b/packages/search/lib/commands/ALIASADD.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/ALIASDEL.ts b/packages/search/lib/commands/ALIASDEL.ts index 3e4b70a1943..9a2dbda4b9e 100644 --- a/packages/search/lib/commands/ALIASDEL.ts +++ b/packages/search/lib/commands/ALIASDEL.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/ALIASUPDATE.ts b/packages/search/lib/commands/ALIASUPDATE.ts index 46f0aa3e020..3bd5ea92ba3 100644 --- a/packages/search/lib/commands/ALIASUPDATE.ts +++ b/packages/search/lib/commands/ALIASUPDATE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/ALTER.ts b/packages/search/lib/commands/ALTER.ts index 4cde32e6ad4..4a68817bd2c 100644 --- a/packages/search/lib/commands/ALTER.ts +++ b/packages/search/lib/commands/ALTER.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { RediSearchSchema, parseSchema } from './CREATE'; export default { diff --git a/packages/search/lib/commands/CONFIG_GET.ts b/packages/search/lib/commands/CONFIG_GET.ts index 9c0be73e2e3..ae7a9e0c78d 100644 --- a/packages/search/lib/commands/CONFIG_GET.ts +++ b/packages/search/lib/commands/CONFIG_GET.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/CONFIG_SET.ts b/packages/search/lib/commands/CONFIG_SET.ts index ae57dd7d8f6..499b9525aa3 100644 --- a/packages/search/lib/commands/CONFIG_SET.ts +++ b/packages/search/lib/commands/CONFIG_SET.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; // using `string & {}` to avoid TS widening the type to `string` // TODO diff --git a/packages/search/lib/commands/CREATE.ts b/packages/search/lib/commands/CREATE.ts index d0096282f37..5eec9799264 100644 --- a/packages/search/lib/commands/CREATE.ts +++ b/packages/search/lib/commands/CREATE.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export const SCHEMA_FIELD_TYPE = { TEXT: 'TEXT', diff --git a/packages/search/lib/commands/CURSOR_DEL.ts b/packages/search/lib/commands/CURSOR_DEL.ts index 1ea4b46c8bd..5f638ebb0ee 100644 --- a/packages/search/lib/commands/CURSOR_DEL.ts +++ b/packages/search/lib/commands/CURSOR_DEL.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { SimpleStringReply, Command, RedisArgument, NumberReply, UnwrapReply } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument, NumberReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/CURSOR_READ.ts b/packages/search/lib/commands/CURSOR_READ.ts index d23a0f0bd15..e64070122d1 100644 --- a/packages/search/lib/commands/CURSOR_READ.ts +++ b/packages/search/lib/commands/CURSOR_READ.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command, NumberReply, UnwrapReply } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command, NumberReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR'; export interface FtCursorReadOptions { diff --git a/packages/search/lib/commands/DICTADD.ts b/packages/search/lib/commands/DICTADD.ts index 67f94a82c13..2106775f854 100644 --- a/packages/search/lib/commands/DICTADD.ts +++ b/packages/search/lib/commands/DICTADD.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/DICTDEL.ts b/packages/search/lib/commands/DICTDEL.ts index 9b0bda3a7a6..988af1139e9 100644 --- a/packages/search/lib/commands/DICTDEL.ts +++ b/packages/search/lib/commands/DICTDEL.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/DICTDUMP.ts b/packages/search/lib/commands/DICTDUMP.ts index 00dd5aba4eb..3c223442ecb 100644 --- a/packages/search/lib/commands/DICTDUMP.ts +++ b/packages/search/lib/commands/DICTDUMP.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/DROPINDEX.ts b/packages/search/lib/commands/DROPINDEX.ts index e7be806ac14..407bdd031aa 100644 --- a/packages/search/lib/commands/DROPINDEX.ts +++ b/packages/search/lib/commands/DROPINDEX.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface FtDropIndexOptions { DD?: true; diff --git a/packages/search/lib/commands/EXPLAIN.ts b/packages/search/lib/commands/EXPLAIN.ts index deb75229b5d..dcd7c3c0d2d 100644 --- a/packages/search/lib/commands/EXPLAIN.ts +++ b/packages/search/lib/commands/EXPLAIN.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { FtSearchParams, parseParamsArgument } from './SEARCH'; export interface FtExplainOptions { diff --git a/packages/search/lib/commands/EXPLAINCLI.ts b/packages/search/lib/commands/EXPLAINCLI.ts index 7a4ae3a4b25..53711067058 100644 --- a/packages/search/lib/commands/EXPLAINCLI.ts +++ b/packages/search/lib/commands/EXPLAINCLI.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/INFO.ts b/packages/search/lib/commands/INFO.ts index 6792645fe3e..cee6ae683cd 100644 --- a/packages/search/lib/commands/INFO.ts +++ b/packages/search/lib/commands/INFO.ts @@ -1,8 +1,8 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument } from "@redis/client"; -import { ArrayReply, BlobStringReply, Command, DoubleReply, MapReply, NullReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/lib/RESP/types"; -import { createTransformTuplesReplyFunc, transformDoubleReply } from "@redis/client/lib/commands/generic-transformers"; -import { TuplesReply } from '@redis/client/lib/RESP/types'; +import { ArrayReply, BlobStringReply, Command, DoubleReply, MapReply, NullReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/dist/lib/RESP/types"; +import { createTransformTuplesReplyFunc, transformDoubleReply } from "@redis/client/dist/lib/commands/generic-transformers"; +import { TuplesReply } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.ts index 703bfcacc72..ad671c9eb45 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, ReplyUnion } from "@redis/client/lib/RESP/types"; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, ReplyUnion } from "@redis/client/dist/lib/RESP/types"; import AGGREGATE, { AggregateRawReply, FtAggregateOptions, parseAggregateOptions } from "./AGGREGATE"; import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from "./PROFILE_SEARCH"; diff --git a/packages/search/lib/commands/PROFILE_SEARCH.ts b/packages/search/lib/commands/PROFILE_SEARCH.ts index c345e70dd78..86ce85ba7f1 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, RedisArgument, ReplyUnion } from "@redis/client/lib/RESP/types"; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, RedisArgument, ReplyUnion } from "@redis/client/dist/lib/RESP/types"; import { AggregateReply } from "./AGGREGATE"; import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, parseSearchOptions } from "./SEARCH"; diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index de03ac0070e..c2d2a9d2696 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command, ReplyUnion } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { RediSearchProperty, RediSearchLanguage } from './CREATE'; export type FtSearchParams = Record; diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.ts index a4c6f6a27aa..c0a152375d9 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.ts @@ -1,4 +1,4 @@ -import { Command, ReplyUnion } from '@redis/client/lib/RESP/types'; +import { Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; import SEARCH, { SearchRawReply } from './SEARCH'; export default { diff --git a/packages/search/lib/commands/SPELLCHECK.ts b/packages/search/lib/commands/SPELLCHECK.ts index 1e6981d01ff..ae95b72c249 100644 --- a/packages/search/lib/commands/SPELLCHECK.ts +++ b/packages/search/lib/commands/SPELLCHECK.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command, ReplyUnion } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; export interface Terms { mode: 'INCLUDE' | 'EXCLUDE'; diff --git a/packages/search/lib/commands/SUGADD.ts b/packages/search/lib/commands/SUGADD.ts index a82f03ffa1b..34e5bccb7f1 100644 --- a/packages/search/lib/commands/SUGADD.ts +++ b/packages/search/lib/commands/SUGADD.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface FtSugAddOptions { INCR?: boolean; diff --git a/packages/search/lib/commands/SUGDEL.ts b/packages/search/lib/commands/SUGDEL.ts index 1cdf56d2025..6bc99456d2e 100644 --- a/packages/search/lib/commands/SUGDEL.ts +++ b/packages/search/lib/commands/SUGDEL.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: true, diff --git a/packages/search/lib/commands/SUGGET.ts b/packages/search/lib/commands/SUGGET.ts index 607e26df94e..e8a3aecdab0 100644 --- a/packages/search/lib/commands/SUGGET.ts +++ b/packages/search/lib/commands/SUGGET.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { NullReply, ArrayReply, BlobStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { NullReply, ArrayReply, BlobStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; export interface FtSugGetOptions { FUZZY?: boolean; diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts index b2112bb4b34..60bf5ee86d9 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.ts @@ -1,5 +1,5 @@ -import { NullReply, ArrayReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; -import { isNullReply } from '@redis/client/lib/commands/generic-transformers'; +import { NullReply, ArrayReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { isNullReply } from '@redis/client/dist/lib/commands/generic-transformers'; import SUGGET from './SUGGET'; export default { diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.ts index 088153c31f5..060e59132db 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.ts @@ -1,5 +1,5 @@ -import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/lib/RESP/types'; -import { isNullReply, transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; +import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; import SUGGET from './SUGGET'; type SuggestScore = { diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts index 6f032a15899..07277420338 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.ts @@ -1,5 +1,5 @@ -import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/lib/RESP/types'; -import { isNullReply, transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; +import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; import SUGGET from './SUGGET'; type SuggestScoreWithPayload = { diff --git a/packages/search/lib/commands/SUGLEN.ts b/packages/search/lib/commands/SUGLEN.ts index 7437559843f..a3f0fbe45ed 100644 --- a/packages/search/lib/commands/SUGLEN.ts +++ b/packages/search/lib/commands/SUGLEN.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: true, diff --git a/packages/search/lib/commands/SYNDUMP.ts b/packages/search/lib/commands/SYNDUMP.ts index 0c3b68bf2e7..5f454f96fe0 100644 --- a/packages/search/lib/commands/SYNDUMP.ts +++ b/packages/search/lib/commands/SYNDUMP.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/SYNUPDATE.ts b/packages/search/lib/commands/SYNUPDATE.ts index 2baf2ded18c..3af735412ae 100644 --- a/packages/search/lib/commands/SYNUPDATE.ts +++ b/packages/search/lib/commands/SYNUPDATE.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { SimpleStringReply, Command, RedisArgument } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface FtSynUpdateOptions { SKIPINITIALSCAN?: boolean; diff --git a/packages/search/lib/commands/TAGVALS.ts b/packages/search/lib/commands/TAGVALS.ts index d00d657f3ab..0afddb247fd 100644 --- a/packages/search/lib/commands/TAGVALS.ts +++ b/packages/search/lib/commands/TAGVALS.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/search/lib/commands/_LIST.ts b/packages/search/lib/commands/_LIST.ts index 432eea64797..c1ca8cc2ee5 100644 --- a/packages/search/lib/commands/_LIST.ts +++ b/packages/search/lib/commands/_LIST.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index e8d7ad45ca6..a1cb63eb7bf 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -2,7 +2,7 @@ import { createConnection } from 'node:net'; import { once } from 'node:events'; import { createClient } from '@redis/client/index'; import { setTimeout } from 'node:timers/promises'; -// import { ClusterSlotsReply } from '@redis/client/lib/commands/CLUSTER_SLOTS'; +// import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS'; import { promisify } from 'node:util'; import { exec } from 'node:child_process'; const execAsync = promisify(exec); diff --git a/packages/time-series/lib/commands/ADD.ts b/packages/time-series/lib/commands/ADD.ts index f79a27c5db7..e7626d227da 100644 --- a/packages/time-series/lib/commands/ADD.ts +++ b/packages/time-series/lib/commands/ADD.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; import { transformTimestampArgument, parseRetentionArgument, diff --git a/packages/time-series/lib/commands/ALTER.ts b/packages/time-series/lib/commands/ALTER.ts index 8217e81c218..f7f6948da71 100644 --- a/packages/time-series/lib/commands/ALTER.ts +++ b/packages/time-series/lib/commands/ALTER.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { TsCreateOptions } from './CREATE'; import { parseRetentionArgument, parseChunkSizeArgument, parseDuplicatePolicy, parseLabelsArgument, parseIgnoreArgument } from '.'; diff --git a/packages/time-series/lib/commands/CREATE.ts b/packages/time-series/lib/commands/CREATE.ts index 86defd1e0a4..39f35c06ed7 100644 --- a/packages/time-series/lib/commands/CREATE.ts +++ b/packages/time-series/lib/commands/CREATE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { parseRetentionArgument, TimeSeriesEncoding, diff --git a/packages/time-series/lib/commands/CREATERULE.ts b/packages/time-series/lib/commands/CREATERULE.ts index 99a8a4c9d57..1e4f38c6ee6 100644 --- a/packages/time-series/lib/commands/CREATERULE.ts +++ b/packages/time-series/lib/commands/CREATERULE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export const TIME_SERIES_AGGREGATION_TYPE = { AVG: 'AVG', diff --git a/packages/time-series/lib/commands/DECRBY.ts b/packages/time-series/lib/commands/DECRBY.ts index c2a7e6abd97..8ff09d926c0 100644 --- a/packages/time-series/lib/commands/DECRBY.ts +++ b/packages/time-series/lib/commands/DECRBY.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import INCRBY, { parseIncrByArguments } from './INCRBY'; export default { diff --git a/packages/time-series/lib/commands/DEL.ts b/packages/time-series/lib/commands/DEL.ts index ad957e6c402..fc96c989b18 100644 --- a/packages/time-series/lib/commands/DEL.ts +++ b/packages/time-series/lib/commands/DEL.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { Timestamp, transformTimestampArgument } from '.'; -import { RedisArgument, NumberReply, Command, } from '@redis/client/lib/RESP/types'; +import { RedisArgument, NumberReply, Command, } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, diff --git a/packages/time-series/lib/commands/DELETERULE.ts b/packages/time-series/lib/commands/DELETERULE.ts index 8a1aa41385d..b4e47a0fba6 100644 --- a/packages/time-series/lib/commands/DELETERULE.ts +++ b/packages/time-series/lib/commands/DELETERULE.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; export default { IS_READ_ONLY: false, diff --git a/packages/time-series/lib/commands/GET.ts b/packages/time-series/lib/commands/GET.ts index 9f165bed6eb..c1bb2c1c749 100644 --- a/packages/time-series/lib/commands/GET.ts +++ b/packages/time-series/lib/commands/GET.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, TuplesReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, TuplesReply, NumberReply, DoubleReply, UnwrapReply, Resp2Reply, Command } from '@redis/client/dist/lib/RESP/types'; export interface TsGetOptions { LATEST?: boolean; diff --git a/packages/time-series/lib/commands/INCRBY.ts b/packages/time-series/lib/commands/INCRBY.ts index 7dbdb6b9ead..e62ec42690a 100644 --- a/packages/time-series/lib/commands/INCRBY.ts +++ b/packages/time-series/lib/commands/INCRBY.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, NumberReply, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; import { Timestamp, transformTimestampArgument, parseRetentionArgument, parseChunkSizeArgument, Labels, parseLabelsArgument, parseIgnoreArgument } from '.'; import { TsIgnoreOptions } from './ADD'; diff --git a/packages/time-series/lib/commands/INFO.ts b/packages/time-series/lib/commands/INFO.ts index fbd66875b1f..fe0e49e095a 100644 --- a/packages/time-series/lib/commands/INFO.ts +++ b/packages/time-series/lib/commands/INFO.ts @@ -1,8 +1,8 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { ArrayReply, BlobStringReply, Command, DoubleReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/lib/RESP/types"; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { ArrayReply, BlobStringReply, Command, DoubleReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/dist/lib/RESP/types"; import { TimeSeriesDuplicatePolicies } from "."; import { TimeSeriesAggregationType } from "./CREATERULE"; -import { transformDoubleReply } from '@redis/client/lib/commands/generic-transformers'; +import { transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers'; export type InfoRawReplyTypes = SimpleStringReply | NumberReply | diff --git a/packages/time-series/lib/commands/INFO_DEBUG.ts b/packages/time-series/lib/commands/INFO_DEBUG.ts index bee1147f2bb..89d66a36ef8 100644 --- a/packages/time-series/lib/commands/INFO_DEBUG.ts +++ b/packages/time-series/lib/commands/INFO_DEBUG.ts @@ -1,5 +1,5 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { BlobStringReply, Command, NumberReply, SimpleStringReply, TypeMapping, ReplyUnion } from "@redis/client/lib/RESP/types"; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { BlobStringReply, Command, NumberReply, SimpleStringReply, TypeMapping, ReplyUnion } from "@redis/client/dist/lib/RESP/types"; import INFO, { InfoRawReply, InfoRawReplyTypes, InfoReply } from "./INFO"; type chunkType = Array<[ diff --git a/packages/time-series/lib/commands/MADD.ts b/packages/time-series/lib/commands/MADD.ts index cb1f077055a..5af94d6d497 100644 --- a/packages/time-series/lib/commands/MADD.ts +++ b/packages/time-series/lib/commands/MADD.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { Timestamp, transformTimestampArgument } from '.'; -import { ArrayReply, NumberReply, SimpleErrorReply, Command } from '@redis/client/lib/RESP/types'; +import { ArrayReply, NumberReply, SimpleErrorReply, Command } from '@redis/client/dist/lib/RESP/types'; export interface TsMAddSample { key: string; diff --git a/packages/time-series/lib/commands/MGET.ts b/packages/time-series/lib/commands/MGET.ts index add742a70d5..fa4e3fc63d6 100644 --- a/packages/time-series/lib/commands/MGET.ts +++ b/packages/time-series/lib/commands/MGET.ts @@ -1,7 +1,7 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, transformSampleReply } from '.'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export interface TsMGetOptions { LATEST?: boolean; diff --git a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts index 67c7dc79600..e93b517f80a 100644 --- a/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MGET_SELECTED_LABELS.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, BlobStringReply, NullReply } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, BlobStringReply, NullReply } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { TsMGetOptions, parseLatestArgument, parseFilterArgument } from './MGET'; import { parseSelectedLabelsArguments } from '.'; import { createTransformMGetLabelsReply } from './MGET_WITHLABELS'; diff --git a/packages/time-series/lib/commands/MGET_WITHLABELS.ts b/packages/time-series/lib/commands/MGET_WITHLABELS.ts index f6d50c91b14..38b8442db31 100644 --- a/packages/time-series/lib/commands/MGET_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MGET_WITHLABELS.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, BlobStringReply, ArrayReply, Resp2Reply, MapReply, TuplesReply, TypeMapping } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { TsMGetOptions, parseLatestArgument, parseFilterArgument } from './MGET'; import { RawLabelValue, resp2MapToValue, resp3MapToValue, SampleRawReply, transformRESP2Labels, transformSampleReply } from '.'; diff --git a/packages/time-series/lib/commands/MRANGE.ts b/packages/time-series/lib/commands/MRANGE.ts index dbe48d6f542..95fa5297bdd 100644 --- a/packages/time-series/lib/commands/MRANGE.ts +++ b/packages/time-series/lib/commands/MRANGE.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; import { TsRangeOptions, parseRangeArguments } from './RANGE'; import { parseFilterArgument } from './MGET'; diff --git a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts index 0d996521d1c..5ccd61b2a26 100644 --- a/packages/time-series/lib/commands/MRANGE_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_GROUPBY.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument, TuplesToMapReply, UnwrapReply } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument, TuplesToMapReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; import { TsRangeOptions, parseRangeArguments } from './RANGE'; import { parseFilterArgument } from './MGET'; diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts index 115944a2f40..643b57a67e7 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, NullReply, RedisArgument } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, NullReply, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { parseSelectedLabelsArguments, resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2Labels, transformSamplesReply } from '.'; import { TsRangeOptions, parseRangeArguments } from './RANGE'; import { parseFilterArgument } from './MGET'; diff --git a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts index b4e8006a84e..c5cf1ef56c5 100644 --- a/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_SELECTED_LABELS_GROUPBY.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, ArrayReply, BlobStringReply, MapReply, TuplesReply, RedisArgument, NullReply } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, MapReply, TuplesReply, RedisArgument, NullReply } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { parseSelectedLabelsArguments, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; import { TsRangeOptions, parseRangeArguments } from './RANGE'; import { extractResp3MRangeSources, parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts index 04d72411b7e..19641596a67 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, UnwrapReply, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, UnwrapReply, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformSamplesReply } from '.'; import { TsRangeOptions, parseRangeArguments } from './RANGE'; import { parseFilterArgument } from './MGET'; diff --git a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts index f09d630dcd4..ff0065e22b7 100644 --- a/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MRANGE_WITHLABELS_GROUPBY.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { Command, ArrayReply, BlobStringReply, Resp2Reply, MapReply, TuplesReply, TypeMapping, RedisArgument } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { resp2MapToValue, resp3MapToValue, SampleRawReply, Timestamp, transformRESP2LabelsWithSources, transformSamplesReply } from '.'; import { TsRangeOptions, parseRangeArguments } from './RANGE'; import { extractResp3MRangeSources, parseGroupByArguments, TsMRangeGroupBy, TsMRangeGroupByRawMetadataReply3 } from './MRANGE_GROUPBY'; diff --git a/packages/time-series/lib/commands/MREVRANGE.ts b/packages/time-series/lib/commands/MREVRANGE.ts index e2ed6d9cc91..99d3123dd27 100644 --- a/packages/time-series/lib/commands/MREVRANGE.ts +++ b/packages/time-series/lib/commands/MREVRANGE.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE, { createTransformMRangeArguments } from './MRANGE'; export default { diff --git a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts index efdd1acdcfe..4afcd113505 100644 --- a/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_GROUPBY.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_GROUPBY, { createTransformMRangeGroupByArguments } from './MRANGE_GROUPBY'; export default { diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts index 8b679e65b6a..10e00fc7a29 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_SELECTED_LABELS, { createTransformMRangeSelectedLabelsArguments } from './MRANGE_SELECTED_LABELS'; export default { diff --git a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts index d01ebe1033a..b000c04c183 100644 --- a/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_SELECTED_LABELS_GROUPBY.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_SELECTED_LABELS_GROUPBY, { createMRangeSelectedLabelsGroupByTransformArguments } from './MRANGE_SELECTED_LABELS_GROUPBY'; export default { diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts index d4f6255592e..6cde143c422 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_WITHLABELS, { createTransformMRangeWithLabelsArguments } from './MRANGE_WITHLABELS'; export default { diff --git a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts index ed43d0eae6b..4727112b974 100644 --- a/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts +++ b/packages/time-series/lib/commands/MREVRANGE_WITHLABELS_GROUPBY.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import MRANGE_WITHLABELS_GROUPBY, { createMRangeWithLabelsGroupByTransformArguments } from './MRANGE_WITHLABELS_GROUPBY'; export default { diff --git a/packages/time-series/lib/commands/QUERYINDEX.ts b/packages/time-series/lib/commands/QUERYINDEX.ts index 69625801974..1b53e84b7a3 100644 --- a/packages/time-series/lib/commands/QUERYINDEX.ts +++ b/packages/time-series/lib/commands/QUERYINDEX.ts @@ -1,6 +1,6 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { ArrayReply, BlobStringReply, SetReply, Command } from '@redis/client/lib/RESP/types'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { ArrayReply, BlobStringReply, SetReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; export default { NOT_KEYED_COMMAND: true, diff --git a/packages/time-series/lib/commands/RANGE.ts b/packages/time-series/lib/commands/RANGE.ts index ef4c79fe603..f7f808cecdb 100644 --- a/packages/time-series/lib/commands/RANGE.ts +++ b/packages/time-series/lib/commands/RANGE.ts @@ -1,8 +1,8 @@ -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RedisArgument, Command } from '@redis/client/lib/RESP/types'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types'; import { Timestamp, transformTimestampArgument, SamplesRawReply, transformSamplesReply } from '.'; import { TimeSeriesAggregationType } from './CREATERULE'; -import { Resp2Reply } from '@redis/client/lib/RESP/types'; +import { Resp2Reply } from '@redis/client/dist/lib/RESP/types'; export const TIME_SERIES_BUCKET_TIMESTAMP = { LOW: '-', diff --git a/packages/time-series/lib/commands/REVRANGE.ts b/packages/time-series/lib/commands/REVRANGE.ts index 24a31a785a2..238b2ce9fe7 100644 --- a/packages/time-series/lib/commands/REVRANGE.ts +++ b/packages/time-series/lib/commands/REVRANGE.ts @@ -1,4 +1,4 @@ -import { Command } from '@redis/client/lib/RESP/types'; +import { Command } from '@redis/client/dist/lib/RESP/types'; import RANGE, { transformRangeArguments } from './RANGE'; export default { diff --git a/packages/time-series/lib/commands/index.ts b/packages/time-series/lib/commands/index.ts index e0389a60a2e..f340861cb96 100644 --- a/packages/time-series/lib/commands/index.ts +++ b/packages/time-series/lib/commands/index.ts @@ -1,4 +1,4 @@ -import type { DoubleReply, NumberReply, RedisCommands, TuplesReply, UnwrapReply, Resp2Reply, ArrayReply, BlobStringReply, MapReply, NullReply, TypeMapping, ReplyUnion, RespType } from '@redis/client/lib/RESP/types'; +import type { DoubleReply, NumberReply, RedisCommands, TuplesReply, UnwrapReply, Resp2Reply, ArrayReply, BlobStringReply, MapReply, NullReply, TypeMapping, ReplyUnion, RespType } from '@redis/client/dist/lib/RESP/types'; import ADD, { TsIgnoreOptions } from './ADD'; import ALTER from './ALTER'; import CREATE from './CREATE'; @@ -29,9 +29,9 @@ import MREVRANGE from './MREVRANGE'; import QUERYINDEX from './QUERYINDEX'; import RANGE from './RANGE'; import REVRANGE from './REVRANGE'; -import { RedisVariadicArgument } from '@redis/client/lib/commands/generic-transformers'; -import { CommandParser } from '@redis/client/lib/client/parser'; -import { RESP_TYPES } from '@redis/client/lib/RESP/decoder'; +import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; +import { CommandParser } from '@redis/client/dist/lib/client/parser'; +import { RESP_TYPES } from '@redis/client/dist/lib/RESP/decoder'; export default { ADD, From b87b8b113430eb5e860ab435dc84e141a2e8ae98 Mon Sep 17 00:00:00 2001 From: Leibale Eidelman Date: Mon, 4 Nov 2024 12:21:41 -0500 Subject: [PATCH 019/105] add createSentinel to the "redis" package --- packages/redis/index.ts | 47 +++++++++++++++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/redis/index.ts b/packages/redis/index.ts index 7586846d12c..572b45b707b 100644 --- a/packages/redis/index.ts +++ b/packages/redis/index.ts @@ -4,12 +4,15 @@ import { RedisScripts, RespVersions, TypeMapping, - createClient as _createClient, + createClient as genericCreateClient, RedisClientOptions, - RedisClientType as _RedisClientType, - createCluster as _createCluster, + RedisClientType as GenericRedisClientType, + createCluster as genericCreateCluster, RedisClusterOptions, - RedisClusterType as _RedisClusterType, + RedisClusterType as genericRedisClusterType, + RedisSentinelOptions, + RedisSentinelType as genericRedisSentinelType, + createSentinel as genericCreateSentinel } from '@redis/client'; import RedisBloomModules from '@redis/bloom'; import RedisGraph from '@redis/graph'; @@ -40,7 +43,7 @@ export type RedisClientType< S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} -> = _RedisClientType; +> = GenericRedisClientType; export function createClient< M extends RedisModules, @@ -50,8 +53,8 @@ export function createClient< TYPE_MAPPING extends TypeMapping >( options?: RedisClientOptions -): _RedisClientType { - return _createClient({ +): GenericRedisClientType { + return genericCreateClient({ ...options, modules: { ...modules, @@ -66,7 +69,7 @@ export type RedisClusterType< S extends RedisScripts = {}, RESP extends RespVersions = 2, TYPE_MAPPING extends TypeMapping = {} -> = _RedisClusterType; +> = genericRedisClusterType; export function createCluster< M extends RedisModules, @@ -77,7 +80,33 @@ export function createCluster< >( options: RedisClusterOptions ): RedisClusterType { - return _createCluster({ + return genericCreateCluster({ + ...options, + modules: { + ...modules, + ...(options?.modules as M) + } + }); +} + +export type RedisSentinelType< + M extends RedisModules = RedisDefaultModules, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} +> = genericRedisSentinelType; + +export function createSentinel< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +>( + options: RedisSentinelOptions +): RedisSentinelType { + return genericCreateSentinel({ ...options, modules: { ...modules, From 9a3e1c5e0327799283053acaf2aa3799cf49cde9 Mon Sep 17 00:00:00 2001 From: Max Gruenfelder Date: Mon, 25 Nov 2024 21:28:44 +0100 Subject: [PATCH 020/105] Fix creation of cluster client again (#2870) * shallow copy of this.#options.defaults.socket * shallow copy of this.#options.defaults.socket * nit * fix redis create cluster client again --------- Co-authored-by: Max Gruenfelder Co-authored-by: Leibale Eidelman --- packages/client/lib/cluster/cluster-slots.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 824cf2ae813..10c2c9d306b 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -261,10 +261,10 @@ export default class RedisClusterSlots< let socket; if (this.#options.defaults.socket) { - socket = options?.socket ? { + socket = { ...this.#options.defaults.socket, - ...options.socket - } : this.#options.defaults.socket; + ...options?.socket + }; } else { socket = options?.socket; } From ffa7d2525c6d8bdd24faaad527a3ad0aec2cd8e9 Mon Sep 17 00:00:00 2001 From: Jeremy <98552694+jjsimps@users.noreply.github.com> Date: Mon, 25 Nov 2024 14:36:33 -0600 Subject: [PATCH 021/105] Fix cluster-slots discover race condition again (#2867) --- packages/client/lib/cluster/cluster-slots.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 10c2c9d306b..3a4adff73c4 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -164,13 +164,14 @@ export default class RedisClusterSlots< } async #discover(rootNode: RedisClusterClientOptions) { - this.#resetSlots(); try { const addressesInUse = new Set(), promises: Array> = [], eagerConnect = this.#options.minimizeConnections !== true; - for (const { from, to, master, replicas } of await this.#getShards(rootNode)) { + const shards = await this.#getShards(rootNode); + this.#resetSlots(); // Reset slots AFTER shards have been fetched to prevent a race condition + for (const { from, to, master, replicas } of shards) { const shard: Shard = { master: this.#initiateSlotNode(master, false, eagerConnect, addressesInUse, promises) }; From ae89341780b529639e47d20c7ce457a213e27f93 Mon Sep 17 00:00:00 2001 From: mohamed amine ozennou <102986762+ozennou@users.noreply.github.com> Date: Wed, 29 Jan 2025 11:40:58 +0100 Subject: [PATCH 022/105] chore: Update tests.yml (#2887) - Add node 22 - Update actions/setup-node from v3 to v4 - Ignore .md files from triggering the workflow --- .github/workflows/tests.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 68ea09c6e4f..9decd26898a 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -6,25 +6,29 @@ on: - master - v4.0 - v5 + paths-ignore: + - '**/*.md' pull_request: branches: - master - v4.0 - v5 + paths-ignore: + - '**/*.md' jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: - node-version: ['18', '20'] + node-version: ['18', '20', '22'] redis-version: ['6.2.6-v17', '7.2.0-v13', '7.4.0-v1'] steps: - uses: actions/checkout@v4 with: fetch-depth: 1 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - name: Update npm From 6d21de3f311876452c938a9c020c4f8a871c0e05 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Thu, 30 Jan 2025 10:29:19 +0200 Subject: [PATCH 023/105] feat(auth): add Entra ID identity provider integration for Redis client authentication (#2877) * feat(auth): refactor authentication mechanism to use CredentialsProvider - Introduce new credential providers: AsyncCredentialsProvider, StreamingCredentialsProvider - Update client handshake process to use the new CredentialsProviders and to support async credentials fetch / credentials refresh - Internal conversion of username/password to a CredentialsProvider - Modify URL parsing to accommodate the new authentication structure - Tests * feat(auth): auth extensions Introduces TokenManager and supporting classes to handle token acquisition, automatic refresh, and updates via identity providers. This foundation enables consistent authentication token management across different identity provider implementations. Key additions: - Add TokenManager to obtain and maintain auth tokens from identity providers with automated refresh scheduling based on TTL and configurable thresholds - Add IdentityProvider interface for token acquisition from auth providers - Implement Token class for managing token state and TTL tracking - Include configurable retry mechanism with exponential backoff and jitter - Add comprehensive test suite covering refresh cycles and error handling This change establishes the core infrastructure needed for reliable token lifecycle management across different authentication providers. * feat(auth): add Entra ID identity provider integration Introduces Entra ID (former Azure AD) authentication support with multiple authentication flows and automated token lifecycle management. Key additions: - Add EntraIdCredentialsProvider for handling Entra ID authentication flows - Implement MSALIdentityProvider to integrate with MSAL/EntraID authentication library - Add support for multiple authentication methods: - Managed identities (system and user-assigned) - Client credentials with certificate - Client credentials with secret - Authorization Code flow with PKCE - Add factory class with builder methods for each authentication flow - Include sample Express server implementation for Authorization Code flow - Add comprehensive configuration options for authority and token management * feat(test-utils): improve cluster testing - Add support for configuring replica authentication with 'masterauth' - Allow default client configuration during test cluster creation This improves the testing framework's flexibility by automatically configuring replica authentication when '--requirepass' is used and enabling custom client configurations across cluster nodes. * feat(auth): add EntraId integration tests - Add integration tests for token renewal and re-authentication flows - Update credentials provider to use uniqueId as username instead of account username - Add test utilities for loading Redis endpoint configurations - Split TypeScript configs into separate files for samples and integration tests - Remove `@redis/authx` package and nest it under `@` --- .github/release-drafter/entraid-config.yml | 50 + .github/workflows/release-drafter-entraid.yml | 24 + package-lock.json | 1150 ++++++++++++++++- .../client/lib/authx/credentials-provider.ts | 102 ++ packages/client/lib/authx/disposable.ts | 6 + .../client/lib/authx/identity-provider.ts | 22 + packages/client/lib/authx/index.ts | 15 + .../client/lib/authx/token-manager.spec.ts | 588 +++++++++ packages/client/lib/authx/token-manager.ts | 318 +++++ packages/client/lib/authx/token.ts | 23 + packages/client/lib/client/index.spec.ts | 121 +- packages/client/lib/client/index.ts | 143 +- packages/client/lib/test-utils.ts | 38 + packages/entraid/.nycrc.json | 10 + packages/entraid/.release-it.json | 11 + packages/entraid/README.md | 137 ++ .../entraid-integration.spec.ts | 217 ++++ .../entra-id-credentials-provider-factory.ts | 371 ++++++ .../lib/entraid-credentials-provider.spec.ts | 199 +++ .../lib/entraid-credentials-provider.ts | 140 ++ packages/entraid/lib/index.ts | 3 + .../entraid/lib/msal-identity-provider.ts | 31 + packages/entraid/lib/test-utils.ts | 46 + packages/entraid/package.json | 47 + .../entraid/samples/auth-code-pkce/index.ts | 153 +++ .../entraid/tsconfig.integration-tests.json | 10 + packages/entraid/tsconfig.json | 20 + packages/entraid/tsconfig.samples.json | 10 + packages/test-utils/lib/cae-client-testing.ts | 30 + packages/test-utils/lib/dockers.ts | 36 +- packages/test-utils/lib/index.ts | 3 +- tsconfig.json | 46 +- 32 files changed, 4004 insertions(+), 116 deletions(-) create mode 100644 .github/release-drafter/entraid-config.yml create mode 100644 .github/workflows/release-drafter-entraid.yml create mode 100644 packages/client/lib/authx/credentials-provider.ts create mode 100644 packages/client/lib/authx/disposable.ts create mode 100644 packages/client/lib/authx/identity-provider.ts create mode 100644 packages/client/lib/authx/index.ts create mode 100644 packages/client/lib/authx/token-manager.spec.ts create mode 100644 packages/client/lib/authx/token-manager.ts create mode 100644 packages/client/lib/authx/token.ts create mode 100644 packages/entraid/.nycrc.json create mode 100644 packages/entraid/.release-it.json create mode 100644 packages/entraid/README.md create mode 100644 packages/entraid/integration-tests/entraid-integration.spec.ts create mode 100644 packages/entraid/lib/entra-id-credentials-provider-factory.ts create mode 100644 packages/entraid/lib/entraid-credentials-provider.spec.ts create mode 100644 packages/entraid/lib/entraid-credentials-provider.ts create mode 100644 packages/entraid/lib/index.ts create mode 100644 packages/entraid/lib/msal-identity-provider.ts create mode 100644 packages/entraid/lib/test-utils.ts create mode 100644 packages/entraid/package.json create mode 100644 packages/entraid/samples/auth-code-pkce/index.ts create mode 100644 packages/entraid/tsconfig.integration-tests.json create mode 100644 packages/entraid/tsconfig.json create mode 100644 packages/entraid/tsconfig.samples.json create mode 100644 packages/test-utils/lib/cae-client-testing.ts diff --git a/.github/release-drafter/entraid-config.yml b/.github/release-drafter/entraid-config.yml new file mode 100644 index 00000000000..d0ddd00773a --- /dev/null +++ b/.github/release-drafter/entraid-config.yml @@ -0,0 +1,50 @@ +name-template: 'entraid@$NEXT_PATCH_VERSION' +tag-template: 'entraid@$NEXT_PATCH_VERSION' +autolabeler: + - label: 'chore' + files: + - '*.md' + - '.github/*' + - label: 'bug' + branch: + - '/bug-.+' + - label: 'chore' + branch: + - '/chore-.+' + - label: 'feature' + branch: + - '/feature-.+' +categories: + - title: 'Breaking Changes' + labels: + - 'breakingchange' + - title: '🚀 New Features' + labels: + - 'feature' + - 'enhancement' + - title: '🐛 Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: '🧰 Maintenance' + label: + - 'chore' + - 'maintenance' + - 'documentation' + - 'docs' + +change-template: '- $TITLE (#$NUMBER)' +include-paths: + - 'packages/entraid' +exclude-labels: + - 'skip-changelog' +template: | + ## Changes + + $CHANGES + + ## Contributors + We'd like to thank all the contributors who worked on this release! + + $CONTRIBUTORS diff --git a/.github/workflows/release-drafter-entraid.yml b/.github/workflows/release-drafter-entraid.yml new file mode 100644 index 00000000000..d522c6cef6f --- /dev/null +++ b/.github/workflows/release-drafter-entraid.yml @@ -0,0 +1,24 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + + update_release_draft: + + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + config-name: release-drafter/entraid-config.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/package-lock.json b/package-lock.json index ba18a98b6a5..8fdd049a5b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,29 @@ "node": ">=6.0.0" } }, + "node_modules/@azure/msal-common": { + "version": "14.16.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", + "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz", + "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "14.16.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@babel/code-frame": { "version": "7.23.5", "dev": true, @@ -824,6 +847,10 @@ "node": ">=12" } }, + "node_modules/@redis/authx": { + "resolved": "packages/authx", + "link": true + }, "node_modules/@redis/bloom": { "resolved": "packages/bloom", "link": true @@ -832,6 +859,10 @@ "resolved": "packages/client", "link": true }, + "node_modules/@redis/entraid": { + "resolved": "packages/entraid", + "link": true + }, "node_modules/@redis/graph": { "resolved": "packages/graph", "link": true @@ -929,11 +960,82 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "dev": true, "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mocha": { "version": "10.0.6", "dev": true, @@ -947,6 +1049,43 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/qs": { + "version": "6.9.17", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/sinon": { "version": "17.0.3", "dev": true, @@ -973,6 +1112,20 @@ "dev": true, "license": "MIT" }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/agent-base": { "version": "7.1.0", "dev": true, @@ -1112,6 +1265,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "license": "MIT" + }, "node_modules/array-union": { "version": "1.0.2", "dev": true, @@ -1260,6 +1420,48 @@ "readable-stream": "^3.4.0" } }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/boxen": { "version": "7.1.1", "dev": true, @@ -1466,6 +1668,12 @@ "ieee754": "^1.1.13" } }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "license": "BSD-3-Clause" + }, "node_modules/bundle-name": { "version": "4.1.0", "dev": true, @@ -1480,6 +1688,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/cacheable-lookup": { "version": "7.0.0", "dev": true, @@ -1531,18 +1749,38 @@ } }, "node_modules/call-bind": { - "version": "1.0.5", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, "license": "MIT", "dependencies": { - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.1", - "set-function-length": "^1.1.1" + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/callsites": { "version": "3.1.0", "dev": true, @@ -1805,11 +2043,51 @@ "url": "https://github.com/yeoman/configstore?sponsor=1" } }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/convert-source-map": { "version": "1.9.0", "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cosmiconfig": { "version": "9.0.0", "dev": true, @@ -2003,16 +2281,21 @@ } }, "node_modules/define-data-property": { - "version": "1.1.1", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.1", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.0" + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" }, "engines": { "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, "node_modules/define-lazy-prop": { @@ -2055,11 +2338,32 @@ "node": ">= 14" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/deprecation": { "version": "2.3.1", "dev": true, "license": "ISC" }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, "node_modules/diff": { "version": "5.0.0", "dev": true, @@ -2082,11 +2386,55 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.0.tgz", + "integrity": "sha512-9+Sj30DIu+4KvHqMfLUGLFYL2PkURSYMVXJyXe92nFRvlYq5hBjLEhblKB+vkd/WVlUYMWigiY07T91Fkk0+4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "dev": true, "license": "MIT" }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.4.656", "dev": true, @@ -2102,6 +2450,16 @@ "dev": true, "license": "MIT" }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/env-paths": { "version": "2.2.1", "dev": true, @@ -2175,6 +2533,16 @@ "dev": true, "license": "MIT" }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es-errors": { "version": "1.3.0", "dev": true, @@ -2292,6 +2660,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "dev": true, @@ -2351,6 +2726,16 @@ "node": ">=0.10.0" } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/execa": { "version": "8.0.1", "dev": true, @@ -2395,6 +2780,131 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-session": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "0.7.2", + "cookie-signature": "1.0.7", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-headers": "~1.0.2", + "parseurl": "~1.3.3", + "safe-buffer": "5.2.1", + "uid-safe": "~2.1.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/express-session/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express-session/node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/express-session/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express-session/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, "node_modules/external-editor": { "version": "3.1.0", "dev": true, @@ -2525,6 +3035,42 @@ "node": ">=8" } }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, "node_modules/find-cache-dir": { "version": "3.3.2", "dev": true, @@ -2603,6 +3149,26 @@ "node": ">=12.20.0" } }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/fromentries": { "version": "1.3.2", "dev": true, @@ -2701,15 +3267,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.3", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.5.tgz", + "integrity": "sha512-Y4+pKa7XeRUPWFNvOOYHkRYrfzW07oraURSvjDmRVOJ748OrVmeXtpE4+GCEHncjCjkTxPNRt8kEbxDhsn6VTg==", "dev": true, "license": "MIT", "dependencies": { - "es-errors": "^1.0.0", + "call-bind-apply-helpers": "^1.0.0", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -2934,11 +3505,13 @@ } }, "node_modules/gopd": { - "version": "1.0.1", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", "dev": true, "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3001,11 +3574,13 @@ } }, "node_modules/has-property-descriptors": { - "version": "1.0.1", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", "dev": true, "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.2" + "es-define-property": "^1.0.0" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3023,7 +3598,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "dev": true, "license": "MIT", "engines": { @@ -3063,7 +3640,9 @@ } }, "node_modules/hasown": { - "version": "2.0.0", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3091,6 +3670,23 @@ "dev": true, "license": "BSD-2-Clause" }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/http-proxy-agent": { "version": "7.0.0", "dev": true, @@ -3360,6 +3956,16 @@ "dev": true, "license": "MIT" }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "dev": true, @@ -4032,26 +4638,81 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.2.1", + "node_modules/jsonc-parser": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonwebtoken": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==", + "license": "MIT", + "dependencies": { + "jws": "^3.2.2", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jsonwebtoken/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", "dev": true, "license": "MIT" }, - "node_modules/jsonfile": { - "version": "6.1.0", - "dev": true, + "node_modules/jwa": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", + "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", "license": "MIT", "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" } }, - "node_modules/just-extend": { - "version": "6.2.0", - "dev": true, - "license": "MIT" + "node_modules/jws": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", + "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "license": "MIT", + "dependencies": { + "jwa": "^1.4.1", + "safe-buffer": "^5.0.1" + } }, "node_modules/keyv": { "version": "4.5.4", @@ -4119,14 +4780,42 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, "node_modules/lodash.isplainobject": { "version": "4.0.6", - "dev": true, "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", - "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, "node_modules/lodash.uniqby": { @@ -4209,6 +4898,26 @@ "node": ">= 12" } }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/merge-stream": { "version": "2.0.0", "dev": true, @@ -4222,6 +4931,16 @@ "node": ">= 8" } }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/micromatch": { "version": "4.0.5", "dev": true, @@ -4234,6 +4953,19 @@ "node": ">=8.6" } }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/mime-db": { "version": "1.52.0", "dev": true, @@ -4384,7 +5116,6 @@ }, "node_modules/ms": { "version": "2.1.3", - "dev": true, "license": "MIT" }, "node_modules/mute-stream": { @@ -4406,6 +5137,16 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/netmask": { "version": "2.0.2", "dev": true, @@ -4723,6 +5464,29 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/once": { "version": "1.4.0", "dev": true, @@ -5178,6 +5942,16 @@ "parse-path": "^7.0.0" } }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/path-exists": { "version": "4.0.0", "dev": true, @@ -5365,6 +6139,20 @@ "dev": true, "license": "MIT" }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/proxy-agent": { "version": "6.3.1", "dev": true, @@ -5410,6 +6198,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "dev": true, @@ -5440,6 +6244,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/random-bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", + "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/randombytes": { "version": "2.1.0", "dev": true, @@ -5448,6 +6262,32 @@ "safe-buffer": "^5.1.0" } }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/rc": { "version": "1.2.8", "dev": true, @@ -5880,7 +6720,6 @@ }, "node_modules/safe-buffer": { "version": "5.2.1", - "dev": true, "funding": [ { "type": "github", @@ -5970,6 +6809,58 @@ "dev": true, "license": "ISC" }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/serialize-javascript": { "version": "6.0.0", "dev": true, @@ -5978,21 +6869,40 @@ "randombytes": "^2.1.0" } }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/set-blocking": { "version": "2.0.0", "dev": true, "license": "ISC" }, "node_modules/set-function-length": { - "version": "1.2.0", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, "license": "MIT", "dependencies": { - "define-data-property": "^1.1.1", + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.2", + "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.1" + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -6011,6 +6921,13 @@ "node": ">= 0.4" } }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, "node_modules/shebang-command": { "version": "2.0.0", "dev": true, @@ -6058,13 +6975,19 @@ } }, "node_modules/side-channel": { - "version": "1.0.4", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6191,6 +7114,16 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/stdin-discarder": { "version": "0.2.2", "dev": true, @@ -6404,6 +7337,16 @@ "node": ">=8.0" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, "node_modules/trim-repeated": { "version": "1.0.0", "dev": true, @@ -6462,6 +7405,20 @@ "node": ">=8" } }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/typed-array-buffer": { "version": "1.0.0", "dev": true, @@ -6585,6 +7542,19 @@ "node": ">=14.17" } }, + "node_modules/uid-safe": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", + "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", + "dev": true, + "license": "MIT", + "dependencies": { + "random-bytes": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/unbox-primitive": { "version": "1.0.2", "dev": true, @@ -6642,6 +7612,16 @@ "node": ">= 10.0.0" } }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/update-browserslist-db": { "version": "1.0.13", "dev": true, @@ -6750,14 +7730,33 @@ "dev": true, "license": "MIT" }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, "node_modules/uuid": { "version": "8.3.2", - "dev": true, "license": "MIT", "bin": { "uuid": "dist/bin/uuid" } }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "dev": true, @@ -7127,6 +8126,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/authx": { + "name": "@redis/authx", + "version": "5.0.0-next.5", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "^2.16.1" + }, + "devDependencies": {}, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/client": "^5.0.0-next.5" + } + }, "packages/bloom": { "name": "@redis/bloom", "version": "5.0.0-next.5", @@ -7155,8 +8169,52 @@ }, "engines": { "node": ">= 18" + }, + "peerDependencies": { + "@redis/authx": "^5.0.0-next.5" + } + }, + "packages/entraid": { + "name": "@redis/entraid", + "version": "5.0.0-next.5", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "^2.16.1" + }, + "devDependencies": { + "@redis/test-utils": "*", + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", + "@types/node": "^22.9.0", + "dotenv": "^16.3.1", + "express": "^4.21.1", + "express-session": "^1.18.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "@redis/authx": "^5.0.0-next.5", + "@redis/client": "^5.0.0-next.5" } }, + "packages/entraid/node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "packages/entraid/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, "packages/graph": { "name": "@redis/graph", "version": "5.0.0-next.5", diff --git a/packages/client/lib/authx/credentials-provider.ts b/packages/client/lib/authx/credentials-provider.ts new file mode 100644 index 00000000000..667795be9b3 --- /dev/null +++ b/packages/client/lib/authx/credentials-provider.ts @@ -0,0 +1,102 @@ +import { Disposable } from './disposable'; +/** + * Provides credentials asynchronously. + */ +export interface AsyncCredentialsProvider { + readonly type: 'async-credentials-provider'; + credentials: () => Promise +} + +/** + * Provides credentials asynchronously with support for continuous updates via a subscription model. + * This is useful for environments where credentials are frequently rotated or updated or can be revoked. + */ +export interface StreamingCredentialsProvider { + readonly type: 'streaming-credentials-provider'; + + /** + * Provides initial credentials and subscribes to subsequent updates. This is used internally by the node-redis client + * to handle credential rotation and re-authentication. + * + * Note: The node-redis client manages the subscription lifecycle automatically. Users only need to implement + * onReAuthenticationError if they want to be notified about authentication failures. + * + * Error handling: + * - Errors received via onError indicate a fatal issue with the credentials stream + * - The stream is automatically closed(disposed) when onError occurs + * - onError typically mean the provider failed to fetch new credentials after retrying + * + * @example + * ```ts + * const provider = getStreamingProvider(); + * const [initialCredentials, disposable] = await provider.subscribe({ + * onNext: (newCredentials) => { + * // Handle credential update + * }, + * onError: (error) => { + * // Handle fatal stream error + * } + * }); + * + * @param listener - Callbacks to handle credential updates and errors + * @returns A Promise resolving to [initial credentials, cleanup function] + */ + subscribe: (listener: StreamingCredentialsListener) => Promise<[BasicAuth, Disposable]> + + /** + * Called when authentication fails or credentials cannot be renewed in time. + * Implement this to handle authentication errors in your application. + * + * @param error - Either a CredentialsError (invalid/expired credentials) or + * UnableToObtainNewCredentialsError (failed to fetch new credentials on time) + */ + onReAuthenticationError: (error: ReAuthenticationError) => void; + +} + +/** + * Type representing basic authentication credentials. + */ +export type BasicAuth = { username?: string, password?: string } + +/** + * Callback to handle credential updates and errors. + */ +export type StreamingCredentialsListener = { + onNext: (credentials: T) => void; + onError: (e: Error) => void; +} + + +/** + * Providers that can supply authentication credentials + */ +export type CredentialsProvider = AsyncCredentialsProvider | StreamingCredentialsProvider + +/** + * Errors that can occur during re-authentication. + */ +export type ReAuthenticationError = CredentialsError | UnableToObtainNewCredentialsError + +/** + * Thrown when re-authentication fails with provided credentials . + * e.g. when the credentials are invalid, expired or revoked. + * + */ +export class CredentialsError extends Error { + constructor(message: string) { + super(`Re-authentication with latest credentials failed: ${message}`); + this.name = 'CredentialsError'; + } + +} + +/** + * Thrown when new credentials cannot be obtained before current ones expire + */ +export class UnableToObtainNewCredentialsError extends Error { + constructor(message: string) { + super(`Unable to obtain new credentials : ${message}`); + this.name = 'UnableToObtainNewCredentialsError'; + } +} \ No newline at end of file diff --git a/packages/client/lib/authx/disposable.ts b/packages/client/lib/authx/disposable.ts new file mode 100644 index 00000000000..ee4526a37bd --- /dev/null +++ b/packages/client/lib/authx/disposable.ts @@ -0,0 +1,6 @@ +/** + * Represents a resource that can be disposed. + */ +export interface Disposable { + dispose(): void; +} \ No newline at end of file diff --git a/packages/client/lib/authx/identity-provider.ts b/packages/client/lib/authx/identity-provider.ts new file mode 100644 index 00000000000..a2d25c8f9db --- /dev/null +++ b/packages/client/lib/authx/identity-provider.ts @@ -0,0 +1,22 @@ +/** + * An identity provider is responsible for providing a token that can be used to authenticate with a service. + */ + +/** + * The response from an identity provider when requesting a token. + * + * note: "native" refers to the type of the token that the actual identity provider library is using. + * + * @type T The type of the native idp token. + * @property token The token. + * @property ttlMs The time-to-live of the token in epoch milliseconds extracted from the native token in local time. + */ +export type TokenResponse = { token: T, ttlMs: number }; + +export interface IdentityProvider { + /** + * Request a token from the identity provider. + * @returns A promise that resolves to an object containing the token and the time-to-live in epoch milliseconds. + */ + requestToken(): Promise>; +} \ No newline at end of file diff --git a/packages/client/lib/authx/index.ts b/packages/client/lib/authx/index.ts new file mode 100644 index 00000000000..ce611e1497f --- /dev/null +++ b/packages/client/lib/authx/index.ts @@ -0,0 +1,15 @@ +export { TokenManager, TokenManagerConfig, TokenStreamListener, RetryPolicy, IDPError } from './token-manager'; +export { + CredentialsProvider, + StreamingCredentialsProvider, + UnableToObtainNewCredentialsError, + CredentialsError, + StreamingCredentialsListener, + AsyncCredentialsProvider, + ReAuthenticationError, + BasicAuth +} from './credentials-provider'; +export { Token } from './token'; +export { IdentityProvider, TokenResponse } from './identity-provider'; + +export { Disposable } from './disposable' \ No newline at end of file diff --git a/packages/client/lib/authx/token-manager.spec.ts b/packages/client/lib/authx/token-manager.spec.ts new file mode 100644 index 00000000000..1cc2a207edc --- /dev/null +++ b/packages/client/lib/authx/token-manager.spec.ts @@ -0,0 +1,588 @@ +import { strict as assert } from 'node:assert'; +import { Token } from './token'; +import { IDPError, RetryPolicy, TokenManager, TokenManagerConfig, TokenStreamListener } from './token-manager'; +import { IdentityProvider, TokenResponse } from './identity-provider'; +import { setTimeout } from 'timers/promises'; + +describe('TokenManager', () => { + + /** + * Helper function to delay execution for a given number of milliseconds. + * @param ms + */ + const delay = (ms: number) => { + return setTimeout(ms); + } + + /** + * IdentityProvider that returns a fixed test token for testing and doesn't handle TTL. + */ + class TestIdentityProvider implements IdentityProvider { + requestToken(): Promise> { + return Promise.resolve({ token: 'test-token 1', ttlMs: 1000 }); + } + } + + /** + * Helper function to create a test token with a given TTL . + * @param ttlMs Time-to-live in milliseconds + */ + const createToken = (ttlMs: number): Token => { + return new Token('test-token', ttlMs, 0); + }; + + /** + * Listener that records received tokens and errors for testing. + */ + class TestListener implements TokenStreamListener { + + public readonly receivedTokens: Token[] = []; + public readonly errors: IDPError[] = []; + + onNext(token: Token): void { + this.receivedTokens.push(token); + } + + onError(error: IDPError): void { + this.errors.push(error); + } + } + + /** + * IdentityProvider that returns a sequence of tokens with a fixed delay simulating network latency. + * Used for testing token refresh scenarios. + */ + class ControlledIdentityProvider implements IdentityProvider { + private tokenIndex = 0; + private readonly delayMs: number; + private readonly ttlMs: number; + + constructor( + private readonly tokens: string[], + delayMs: number = 0, + tokenTTlMs: number = 100 + ) { + this.delayMs = delayMs; + this.ttlMs = tokenTTlMs; + } + + async requestToken(): Promise> { + + if (this.tokenIndex >= this.tokens.length) { + throw new Error('No more test tokens available'); + } + + if (this.delayMs > 0) { + await setTimeout(this.delayMs); + } + + return { token: this.tokens[this.tokenIndex++], ttlMs: this.ttlMs }; + } + + } + + /** + * IdentityProvider that simulates various error scenarios with configurable behavior + */ + class ErrorSimulatingProvider implements IdentityProvider { + private requestCount = 0; + + constructor( + private readonly errorSequence: Array, + private readonly delayMs: number = 0, + private readonly ttlMs: number = 100 + ) {} + + async requestToken(): Promise> { + + if (this.delayMs > 0) { + await delay(this.delayMs); + } + + const result = this.errorSequence[this.requestCount]; + this.requestCount++; + + if (result instanceof Error) { + throw result; + } else if (typeof result === 'string') { + return { token: result, ttlMs: this.ttlMs }; + } else { + throw new Error('No more responses configured'); + } + } + + getRequestCount(): number { + return this.requestCount; + } + } + + describe('constructor validation', () => { + it('should throw error if ratio is greater than 1', () => { + const config: TokenManagerConfig = { + expirationRefreshRatio: 1.1 + }; + + assert.throws( + () => new TokenManager(new TestIdentityProvider(), config), + /expirationRefreshRatio must be less than or equal to 1/ + ); + }); + + it('should throw error if ratio is negative', () => { + const config: TokenManagerConfig = { + expirationRefreshRatio: -0.1 + }; + + assert.throws( + () => new TokenManager(new TestIdentityProvider(), config), + /expirationRefreshRatio must be greater or equal to 0/ + ); + }); + + it('should accept ratio of 1', () => { + const config: TokenManagerConfig = { + expirationRefreshRatio: 1 + }; + + assert.doesNotThrow( + () => new TokenManager(new TestIdentityProvider(), config) + ); + }); + + it('should accept ratio of 0', () => { + const config: TokenManagerConfig = { + expirationRefreshRatio: 0 + }; + + assert.doesNotThrow( + () => new TokenManager(new TestIdentityProvider(), config) + ); + }); + }); + + describe('calculateRefreshTime', () => { + it('should calculate correct refresh time with 0.8 ratio', () => { + const config: TokenManagerConfig = { + expirationRefreshRatio: 0.8 + }; + + const manager = new TokenManager(new TestIdentityProvider(), config); + const token = createToken(1000); + const refreshTime = manager.calculateRefreshTime(token, 0); + + // With 1000s TTL and 0.8 ratio, should refresh at 800s + assert.equal(refreshTime, 800); + }); + + it('should return 0 for ratio of 0', () => { + const config: TokenManagerConfig = { + expirationRefreshRatio: 0 + }; + + const manager = new TokenManager(new TestIdentityProvider(), config); + const token = createToken(1000); + const refreshTime = manager.calculateRefreshTime(token, 0); + + assert.equal(refreshTime, 0); + }); + + it('should refresh at expiration time with ratio of 1', () => { + const config: TokenManagerConfig = { + expirationRefreshRatio: 1 + }; + + const manager = new TokenManager(new TestIdentityProvider(), config); + const token = createToken(1000); + const refreshTime = manager.calculateRefreshTime(token, 0); + + assert.equal(refreshTime, 1000); + }); + + it('should handle short TTL tokens', () => { + const config: TokenManagerConfig = { + expirationRefreshRatio: 0.8 + }; + + const manager = new TokenManager(new TestIdentityProvider(), config); + const token = createToken(5); + const refreshTime = manager.calculateRefreshTime(token, 0); + + assert.equal(refreshTime, 4); + }); + + it('should handle expired tokens', () => { + const config: TokenManagerConfig = { + expirationRefreshRatio: 0.8 + }; + + const manager = new TokenManager(new TestIdentityProvider(), config); + // Create token that expired 100s ago + const token = createToken(-100); + const refreshTime = manager.calculateRefreshTime(token, 0); + + // Should return refresh time of 0 for expired tokens + assert.equal(refreshTime, 0); + }); + describe('token refresh scenarios', () => { + + describe('token refresh', () => { + it('should handle token refresh', async () => { + const networkDelay = 20; + const tokenTtl = 100; + + const config: TokenManagerConfig = { + expirationRefreshRatio: 0.8 + }; + + const identityProvider = new ControlledIdentityProvider(['token1', 'token2', 'token3'], networkDelay, tokenTtl); + const manager = new TokenManager(identityProvider, config); + const listener = new TestListener(); + const disposable = manager.start(listener); + + assert.equal(manager.getCurrentToken(), null, 'Should not have token yet'); + // Wait for the first token request to complete ( it should be immediate, and we should wait only for the network delay) + await delay(networkDelay) + + assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token'); + assert.equal(listener.receivedTokens[0].value, 'token1', 'Should have correct token value'); + assert.equal(listener.receivedTokens[0].expiresAtMs - listener.receivedTokens[0].receivedAtMs, + tokenTtl, 'Should have correct TTL'); + assert.equal(listener.errors.length, 0, 'Should not have any errors: ' + listener.errors); + assert.equal(manager.getCurrentToken().value, 'token1', 'Should have current token'); + + await delay(80); + + assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet'); + assert.equal(listener.errors.length, 0, 'Should not have any errors'); + + await delay(networkDelay); + + assert.equal(listener.receivedTokens.length, 2, 'Should receive second token'); + assert.equal(listener.receivedTokens[1].value, 'token2', 'Should have correct token value'); + assert.equal(listener.receivedTokens[1].expiresAtMs - listener.receivedTokens[1].receivedAtMs, + tokenTtl, 'Should have correct TTL'); + assert.equal(listener.errors.length, 0, 'Should not have any errors'); + assert.equal(manager.getCurrentToken().value, 'token2', 'Should have current token'); + + await delay(80); + + assert.equal(listener.receivedTokens.length, 2, 'Should not receive new token yet'); + assert.equal(listener.errors.length, 0, 'Should not have any errors'); + + await delay(networkDelay); + + assert.equal(listener.receivedTokens.length, 3, 'Should receive third token'); + assert.equal(listener.receivedTokens[2].value, 'token3', 'Should have correct token value'); + assert.equal(listener.receivedTokens[2].expiresAtMs - listener.receivedTokens[2].receivedAtMs, + tokenTtl, 'Should have correct TTL'); + assert.equal(listener.errors.length, 0, 'Should not have any errors'); + assert.equal(manager.getCurrentToken().value, 'token3', 'Should have current token'); + + disposable?.dispose(); + }); + }); + }); + }); + + describe('TokenManager error handling', () => { + + describe('error scenarios', () => { + it('should not recover if retries are not enabled', async () => { + + const networkDelay = 20; + const tokenTtl = 100; + + const config: TokenManagerConfig = { + expirationRefreshRatio: 0.8 + }; + + const identityProvider = new ErrorSimulatingProvider( + [ + 'token1', + new Error('Fatal error'), + 'token3' + ], + networkDelay, + tokenTtl + ); + + const manager = new TokenManager(identityProvider, config); + const listener = new TestListener(); + const disposable = manager.start(listener); + + await delay(networkDelay); + + assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token'); + assert.equal(listener.receivedTokens[0].value, 'token1', 'Should have correct initial token'); + assert.equal(listener.receivedTokens[0].expiresAtMs - listener.receivedTokens[0].receivedAtMs, + tokenTtl, 'Should have correct TTL'); + assert.equal(listener.errors.length, 0, 'Should not have errors yet'); + + await delay(80); + + assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet'); + assert.equal(listener.errors.length, 0, 'Should not have any errors'); + + await delay(networkDelay); + + assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token after failure'); + assert.equal(listener.errors.length, 1, 'Should receive error'); + assert.equal(listener.errors[0].message, 'Fatal error', 'Should have correct error message'); + assert.equal(listener.errors[0].isRetryable, false, 'Should be a fatal error'); + + // verify that the token manager is stopped and no more requests are made after the error and expected refresh time + await delay(80); + + assert.equal(identityProvider.getRequestCount(), 2, 'Should not make more requests after error'); + assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token after error'); + assert.equal(listener.errors.length, 1, 'Should not receive more errors after error'); + assert.equal(manager.isRunning(), false, 'Should stop token manager after error'); + + disposable?.dispose(); + }); + + it('should handle retries with exponential backoff', async () => { + const networkDelay = 20; + const tokenTtl = 100; + + const config: TokenManagerConfig = { + expirationRefreshRatio: 0.8, + retry: { + maxAttempts: 3, + initialDelayMs: 100, + maxDelayMs: 1000, + backoffMultiplier: 2, + isRetryable: (error: unknown) => error instanceof Error && error.message === 'Temporary failure' + } + }; + + const identityProvider = new ErrorSimulatingProvider( + [ + 'initial-token', + new Error('Temporary failure'), // First attempt fails + new Error('Temporary failure'), // First retry fails + 'recovery-token' // Second retry succeeds + ], + networkDelay, + tokenTtl + ); + + const manager = new TokenManager(identityProvider, config); + const listener = new TestListener(); + const disposable = manager.start(listener); + + // Wait for initial token + await delay(networkDelay); + assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token'); + assert.equal(listener.receivedTokens[0].value, 'initial-token', 'Should have correct initial token'); + assert.equal(listener.receivedTokens[0].expiresAtMs - listener.receivedTokens[0].receivedAtMs, + tokenTtl, 'Should have correct TTL'); + assert.equal(listener.errors.length, 0, 'Should not have errors yet'); + + await delay(80); + + assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet'); + assert.equal(listener.errors.length, 0, 'Should not have any errors'); + + await delay(networkDelay); + + // Should have first error but not stop due to retry config + assert.equal(listener.errors.length, 1, 'Should have first error'); + assert.ok(listener.errors[0].message.includes('attempt 1'), 'Error should indicate first attempt'); + assert.equal(listener.errors[0].isRetryable, true, 'Should not be a fatal error'); + assert.equal(manager.isRunning(), true, 'Should continue running during retries'); + + // Advance past first retry (delay: 100ms due to backoff) + await delay(100); + + assert.equal(listener.errors.length, 1, 'Should not have the second error yet'); + + await delay(networkDelay); + + assert.equal(listener.errors.length, 2, 'Should have second error'); + assert.ok(listener.errors[1].message.includes('attempt 2'), 'Error should indicate second attempt'); + assert.equal(listener.errors[0].isRetryable, true, 'Should not be a fatal error'); + assert.equal(manager.isRunning(), true, 'Should continue running during retries'); + + // Advance past second retry (delay: 200ms due to backoff) + await delay(200); + + assert.equal(listener.errors.length, 2, 'Should not have another error'); + assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet'); + + await delay(networkDelay); + + // Should have recovered + assert.equal(listener.receivedTokens.length, 2, 'Should receive recovery token'); + assert.equal(listener.receivedTokens[1].value, 'recovery-token', 'Should have correct recovery token'); + assert.equal(listener.receivedTokens[1].expiresAtMs - listener.receivedTokens[1].receivedAtMs, + tokenTtl, 'Should have correct TTL'); + assert.equal(manager.isRunning(), true, 'Should continue running after recovery'); + assert.equal(identityProvider.getRequestCount(), 4, 'Should have made exactly 4 requests'); + + disposable?.dispose(); + }); + + it('should stop after max retries exceeded', async () => { + const networkDelay = 20; + const tokenTtl = 100; + + const config: TokenManagerConfig = { + expirationRefreshRatio: 0.8, + retry: { + maxAttempts: 2, // Only allow 2 retries + initialDelayMs: 100, + maxDelayMs: 1000, + backoffMultiplier: 2, + jitterPercentage: 0, + isRetryable: (error: unknown) => error instanceof Error && error.message === 'Temporary failure' + } + }; + + // All attempts must fail + const identityProvider = new ErrorSimulatingProvider( + [ + 'initial-token', + new Error('Temporary failure'), + new Error('Temporary failure'), + new Error('Temporary failure') + ], + networkDelay, + tokenTtl + ); + + const manager = new TokenManager(identityProvider, config); + const listener = new TestListener(); + const disposable = manager.start(listener); + + // Wait for initial token + await delay(networkDelay); + assert.equal(listener.receivedTokens.length, 1, 'Should receive initial token'); + + await delay(80); + + assert.equal(listener.receivedTokens.length, 1, 'Should not receive new token yet'); + assert.equal(listener.errors.length, 0, 'Should not have any errors'); + + //wait for the "network call" to complete + await delay(networkDelay); + + // First error + assert.equal(listener.errors.length, 1, 'Should have first error'); + assert.equal(manager.isRunning(), true, 'Should continue running after first error'); + assert.equal(listener.errors[0].isRetryable, true, 'Should not be a fatal error'); + + // Advance past first retry + await delay(100); + + assert.equal(listener.errors.length, 1, 'Should not have second error yet'); + + //wait for the "network call" to complete + await delay(networkDelay); + + // Second error + assert.equal(listener.errors.length, 2, 'Should have second error'); + assert.equal(manager.isRunning(), true, 'Should continue running after second error'); + assert.equal(listener.errors[1].isRetryable, true, 'Should not be a fatal error'); + + // Advance past second retry + await delay(200); + + assert.equal(listener.errors.length, 2, 'Should not have third error yet'); + + //wait for the "network call" to complete + await delay(networkDelay); + + // Should stop after max retries + assert.equal(listener.errors.length, 3, 'Should have final error'); + assert.equal(listener.errors[2].isRetryable, false, 'Should be a fatal error'); + assert.equal(manager.isRunning(), false, 'Should stop after max retries exceeded'); + assert.equal(identityProvider.getRequestCount(), 4, 'Should have made exactly 4 requests'); + + disposable?.dispose(); + + }); + }); + }); + + describe('TokenManager retry delay calculations', () => { + const createManager = (retryConfig: Partial) => { + const config: TokenManagerConfig = { + expirationRefreshRatio: 0.8, + retry: { + maxAttempts: 3, + initialDelayMs: 100, + maxDelayMs: 1000, + backoffMultiplier: 2, + ...retryConfig + } + }; + return new TokenManager(new TestIdentityProvider(), config); + }; + + describe('calculateRetryDelay', () => { + + it('should apply exponential backoff', () => { + const manager = createManager({ + initialDelayMs: 100, + backoffMultiplier: 2, + jitterPercentage: 0 + }); + + // Test multiple retry attempts + const expectedDelays = [ + [1, 100], // First attempt: initialDelay * (2^0) = 100 + [2, 200], // Second attempt: initialDelay * (2^1) = 200 + [3, 400], // Third attempt: initialDelay * (2^2) = 400 + [4, 800], // Fourth attempt: initialDelay * (2^3) = 800 + [5, 1000] // Fifth attempt: would be 1600, but capped at maxDelay (1000) + ]; + + for (const [attempt, expectedDelay] of expectedDelays) { + manager['retryAttempt'] = attempt; + assert.equal( + manager.calculateRetryDelay(), + expectedDelay, + `Incorrect delay for attempt ${attempt}` + ); + } + }); + + it('should respect maxDelayMs', () => { + const manager = createManager({ + initialDelayMs: 100, + maxDelayMs: 300, + backoffMultiplier: 2, + jitterPercentage: 0 + }); + + // Test that delays are capped at maxDelayMs + const expectedDelays = [ + [1, 100], // First attempt: 100 + [2, 200], // Second attempt: 200 + [3, 300], // Third attempt: would be 400, capped at 300 + [4, 300], // Fourth attempt: would be 800, capped at 300 + [5, 300] // Fifth attempt: would be 1600, capped at 300 + ]; + + for (const [attempt, expectedDelay] of expectedDelays) { + manager['retryAttempt'] = attempt; + assert.equal( + manager.calculateRetryDelay(), + expectedDelay, + `Incorrect delay for attempt ${attempt}` + ); + } + }); + + it('should return 0 when no retry config is present', () => { + const manager = new TokenManager(new TestIdentityProvider(), { + expirationRefreshRatio: 0.8 + }); + manager['retryAttempt'] = 1; + assert.equal(manager.calculateRetryDelay(), 0); + }); + }); + }); +}); + diff --git a/packages/client/lib/authx/token-manager.ts b/packages/client/lib/authx/token-manager.ts new file mode 100644 index 00000000000..6532d88317b --- /dev/null +++ b/packages/client/lib/authx/token-manager.ts @@ -0,0 +1,318 @@ +import { IdentityProvider, TokenResponse } from './identity-provider'; +import { Token } from './token'; +import {Disposable} from './disposable'; + +/** + * The configuration for retrying token refreshes. + */ +export interface RetryPolicy { + /** + * The maximum number of attempts to retry token refreshes. + */ + maxAttempts: number; + + /** + * The initial delay in milliseconds before the first retry. + */ + initialDelayMs: number; + + /** + * The maximum delay in milliseconds between retries. + * The calculated delay will be capped at this value. + */ + maxDelayMs: number; + + /** + * The multiplier for exponential backoff between retries. + * @example + * A value of 2 will double the delay each time: + * - 1st retry: initialDelayMs + * - 2nd retry: initialDelayMs * 2 + * - 3rd retry: initialDelayMs * 4 + */ + backoffMultiplier: number; + + /** + * The percentage of jitter to apply to the delay. + * @example + * A value of 0.1 will add or subtract up to 10% of the delay. + */ + jitterPercentage?: number; + + /** + * Function to classify errors from the identity provider as retryable or non-retryable. + * Used to determine if a token refresh failure should be retried based on the type of error. + * + * The default behavior is to retry all types of errors if no function is provided. + * + * Common use cases: + * - Network errors that may be transient (should retry) + * - Invalid credentials (should not retry) + * - Rate limiting responses (should retry) + * + * @param error - The error from the identity provider3 + * @param attempt - Current retry attempt (0-based) + * @returns `true` if the error is considered transient and the operation should be retried + * + * @example + * ```typescript + * const retryPolicy: RetryPolicy = { + * maxAttempts: 3, + * initialDelayMs: 1000, + * maxDelayMs: 5000, + * backoffMultiplier: 2, + * isRetryable: (error) => { + * // Retry on network errors or rate limiting + * return error instanceof NetworkError || + * error instanceof RateLimitError; + * } + * }; + * ``` + */ + isRetryable?: (error: unknown, attempt: number) => boolean; +} + +/** + * the configuration for the TokenManager. + */ +export interface TokenManagerConfig { + + /** + * Represents the ratio of a token's lifetime at which a refresh should be triggered. + * For example, a value of 0.75 means the token should be refreshed when 75% of its lifetime has elapsed (or when + * 25% of its lifetime remains). + */ + expirationRefreshRatio: number; + + // The retry policy for token refreshes. If not provided, no retries will be attempted. + retry?: RetryPolicy; +} + +/** + * IDPError indicates a failure from the identity provider. + * + * The `isRetryable` flag is determined by the RetryPolicy's error classification function - if an error is + * classified as retryable, it will be marked as transient and the token manager will attempt to recover. + */ +export class IDPError extends Error { + constructor(public readonly message: string, public readonly isRetryable: boolean) { + super(message); + this.name = 'IDPError'; + } +} + +/** + * TokenStreamListener is an interface for objects that listen to token changes. + */ +export type TokenStreamListener = { + /** + * Called each time a new token is received. + * @param token + */ + onNext: (token: Token) => void; + + /** + * Called when an error occurs while calling the underlying IdentityProvider. The error can be + * transient and the token manager will attempt to obtain a token again if retry policy is configured. + * + * Only fatal errors will terminate the stream and stop the token manager. + * + * @param error + */ + onError: (error: IDPError) => void; + +} + +/** + * TokenManager is responsible for obtaining/refreshing tokens and notifying listeners about token changes. + * It uses an IdentityProvider to request tokens. The token refresh is scheduled based on the token's TTL and + * the expirationRefreshRatio configuration. + * + * The TokenManager should be disposed when it is no longer needed by calling the dispose method on the Disposable + * returned by start. + */ +export class TokenManager { + private currentToken: Token | null = null; + private refreshTimeout: NodeJS.Timeout | null = null; + private listener: TokenStreamListener | null = null; + private retryAttempt: number = 0; + + constructor( + private readonly identityProvider: IdentityProvider, + private readonly config: TokenManagerConfig + ) { + if (this.config.expirationRefreshRatio > 1) { + throw new Error('expirationRefreshRatio must be less than or equal to 1'); + } + if (this.config.expirationRefreshRatio < 0) { + throw new Error('expirationRefreshRatio must be greater or equal to 0'); + } + } + + /** + * Starts the token manager and returns a Disposable that can be used to stop the token manager. + * + * @param listener The listener that will receive token updates. + * @param initialDelayMs The initial delay in milliseconds before the first token refresh. + */ + public start(listener: TokenStreamListener, initialDelayMs: number = 0): Disposable { + if (this.listener) { + this.stop(); + } + + this.listener = listener; + this.retryAttempt = 0; + + this.scheduleNextRefresh(initialDelayMs); + + return { + dispose: () => this.stop() + }; + } + + public calculateRetryDelay(): number { + if (!this.config.retry) return 0; + + const { initialDelayMs, maxDelayMs, backoffMultiplier, jitterPercentage } = this.config.retry; + + let delay = initialDelayMs * Math.pow(backoffMultiplier, this.retryAttempt - 1); + + delay = Math.min(delay, maxDelayMs); + + if (jitterPercentage) { + const jitterRange = delay * (jitterPercentage / 100); + const jitterAmount = Math.random() * jitterRange - (jitterRange / 2); + delay += jitterAmount; + } + + let result = Math.max(0, Math.floor(delay)); + + return result; + } + + private shouldRetry(error: unknown): boolean { + if (!this.config.retry) return false; + + const { maxAttempts, isRetryable } = this.config.retry; + + if (this.retryAttempt >= maxAttempts) { + return false; + } + + if (isRetryable) { + return isRetryable(error, this.retryAttempt); + } + + return false; + } + + public isRunning(): boolean { + return this.listener !== null; + } + + private async refresh(): Promise { + if (!this.listener) { + throw new Error('TokenManager is not running, but refresh was called'); + } + + try { + await this.identityProvider.requestToken().then(this.handleNewToken); + this.retryAttempt = 0; + } catch (error) { + + if (this.shouldRetry(error)) { + this.retryAttempt++; + const retryDelay = this.calculateRetryDelay(); + this.notifyError(`Token refresh failed (attempt ${this.retryAttempt}), retrying in ${retryDelay}ms: ${error}`, true) + this.scheduleNextRefresh(retryDelay); + } else { + this.notifyError(error, false); + this.stop(); + } + } + } + + private handleNewToken = async ({ token: nativeToken, ttlMs }: TokenResponse): Promise => { + if (!this.listener) { + throw new Error('TokenManager is not running, but a new token was received'); + } + const token = this.wrapAndSetCurrentToken(nativeToken, ttlMs); + this.listener.onNext(token); + + this.scheduleNextRefresh(this.calculateRefreshTime(token)); + } + + /** + * Creates a Token object from a native token and sets it as the current token. + * + * @param nativeToken - The raw token received from the identity provider + * @param ttlMs - Time-to-live in milliseconds for the token + * + * @returns A new Token instance containing the wrapped native token and expiration details + * + */ + public wrapAndSetCurrentToken(nativeToken: T, ttlMs: number): Token { + const now = Date.now(); + const token = new Token( + nativeToken, + now + ttlMs, + now + ); + this.currentToken = token; + return token; + } + + private scheduleNextRefresh(delayMs: number): void { + if (this.refreshTimeout) { + clearTimeout(this.refreshTimeout); + this.refreshTimeout = null; + } + if (delayMs === 0) { + this.refresh(); + } else { + this.refreshTimeout = setTimeout(() => this.refresh(), delayMs); + } + + } + + /** + * Calculates the time in milliseconds when the token should be refreshed + * based on the token's TTL and the expirationRefreshRatio configuration. + * + * @param token The token to calculate the refresh time for. + * @param now The current time in milliseconds. Defaults to Date.now(). + */ + public calculateRefreshTime(token: Token, now: number = Date.now()): number { + const ttlMs = token.getTtlMs(now); + return Math.floor(ttlMs * this.config.expirationRefreshRatio); + } + + private stop(): void { + + if (this.refreshTimeout) { + clearTimeout(this.refreshTimeout); + this.refreshTimeout = null; + } + + this.listener = null; + this.currentToken = null; + this.retryAttempt = 0; + } + + /** + * Returns the current token or null if no token is available. + */ + public getCurrentToken(): Token | null { + return this.currentToken; + } + + private notifyError(error: unknown, isRetryable: boolean): void { + const errorMessage = error instanceof Error ? error.message : String(error); + + if (!this.listener) { + throw new Error(`TokenManager is not running but received an error: ${errorMessage}`); + } + + this.listener.onError(new IDPError(errorMessage, isRetryable)); + } +} \ No newline at end of file diff --git a/packages/client/lib/authx/token.ts b/packages/client/lib/authx/token.ts new file mode 100644 index 00000000000..3d6e6867d84 --- /dev/null +++ b/packages/client/lib/authx/token.ts @@ -0,0 +1,23 @@ +/** + * A token that can be used to authenticate with a service. + */ +export class Token { + constructor( + public readonly value: T, + //represents the token deadline - the time in milliseconds since the Unix epoch at which the token expires + public readonly expiresAtMs: number, + //represents the time in milliseconds since the Unix epoch at which the token was received + public readonly receivedAtMs: number + ) {} + + /** + * Returns the time-to-live of the token in milliseconds. + * @param now The current time in milliseconds since the Unix epoch. + */ + getTtlMs(now: number): number { + if (this.expiresAtMs < now) { + return 0; + } + return this.expiresAtMs - now; + } +} \ No newline at end of file diff --git a/packages/client/lib/client/index.spec.ts b/packages/client/lib/client/index.spec.ts index cd2040ec97f..c71cf1a1fad 100644 --- a/packages/client/lib/client/index.spec.ts +++ b/packages/client/lib/client/index.spec.ts @@ -1,6 +1,6 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL, waitTillBeenCalled } from '../test-utils'; -import RedisClient, { RedisClientType } from '.'; +import RedisClient, { RedisClientOptions, RedisClientType } from '.'; import { AbortError, ClientClosedError, ClientOfflineError, ConnectionTimeoutError, DisconnectsClientError, ErrorReply, MultiErrorReply, SocketClosedUnexpectedlyError, WatchError } from '../errors'; import { defineScript } from '../lua-script'; import { spy } from 'sinon'; @@ -25,36 +25,87 @@ export const SQUARE_SCRIPT = defineScript({ describe('Client', () => { describe('parseURL', () => { - it('redis://user:secret@localhost:6379/0', () => { - assert.deepEqual( - RedisClient.parseURL('redis://user:secret@localhost:6379/0'), - { - socket: { - host: 'localhost', - port: 6379 - }, - username: 'user', - password: 'secret', - database: 0 + it('redis://user:secret@localhost:6379/0', async () => { + const result = RedisClient.parseURL('redis://user:secret@localhost:6379/0'); + const expected : RedisClientOptions = { + socket: { + host: 'localhost', + port: 6379 + }, + username: 'user', + password: 'secret', + database: 0, + credentialsProvider: { + type: 'async-credentials-provider', + credentials: async () => ({ + password: 'secret', + username: 'user' + }) } - ); + }; + + // Compare everything except the credentials function + const { credentialsProvider: resultCredProvider, ...resultRest } = result; + const { credentialsProvider: expectedCredProvider, ...expectedRest } = expected; + + // Compare non-function properties + assert.deepEqual(resultRest, expectedRest); + + if(result.credentialsProvider.type === 'async-credentials-provider' + && expected.credentialsProvider.type === 'async-credentials-provider') { + + // Compare the actual output of the credentials functions + const resultCreds = await result.credentialsProvider.credentials(); + const expectedCreds = await expected.credentialsProvider.credentials(); + assert.deepEqual(resultCreds, expectedCreds); + } else { + assert.fail('Credentials provider type mismatch'); + } + + }); - it('rediss://user:secret@localhost:6379/0', () => { - assert.deepEqual( - RedisClient.parseURL('rediss://user:secret@localhost:6379/0'), - { - socket: { - host: 'localhost', - port: 6379, - tls: true - }, - username: 'user', - password: 'secret', - database: 0 + it('rediss://user:secret@localhost:6379/0', async () => { + const result = RedisClient.parseURL('rediss://user:secret@localhost:6379/0'); + const expected: RedisClientOptions = { + socket: { + host: 'localhost', + port: 6379, + tls: true + }, + username: 'user', + password: 'secret', + database: 0, + credentialsProvider: { + credentials: async () => ({ + password: 'secret', + username: 'user' + }), + type: 'async-credentials-provider' } - ); - }); + }; + + // Compare everything except the credentials function + const { credentialsProvider: resultCredProvider, ...resultRest } = result; + const { credentialsProvider: expectedCredProvider, ...expectedRest } = expected; + + // Compare non-function properties + assert.deepEqual(resultRest, expectedRest); + assert.equal(resultCredProvider.type, expectedCredProvider.type); + + if (result.credentialsProvider.type === 'async-credentials-provider' && + expected.credentialsProvider.type === 'async-credentials-provider') { + + // Compare the actual output of the credentials functions + const resultCreds = await result.credentialsProvider.credentials(); + const expectedCreds = await expected.credentialsProvider.credentials(); + assert.deepEqual(resultCreds, expectedCreds); + + } else { + assert.fail('Credentials provider type mismatch'); + } + + }) it('Invalid protocol', () => { assert.throws( @@ -90,6 +141,21 @@ describe('Client', () => { ); }, GLOBAL.SERVERS.PASSWORD); + testUtils.testWithClient('Client can authenticate asynchronously ', async client => { + assert.equal( + await client.ping(), + 'PONG' + ); + }, GLOBAL.SERVERS.ASYNC_BASIC_AUTH); + + testUtils.testWithClient('Client can authenticate using the streaming credentials provider for initial token acquisition', + async client => { + assert.equal( + await client.ping(), + 'PONG' + ); + }, GLOBAL.SERVERS.STREAMING_AUTH); + testUtils.testWithClient('should execute AUTH before SELECT', async client => { assert.equal( (await client.clientInfo()).db, @@ -294,6 +360,7 @@ describe('Client', () => { assert.equal(err.replies.length, 2); assert.deepEqual(err.errorIndexes, [1]); assert.ok(err.replies[1] instanceof ErrorReply); + // @ts-ignore TS2802 assert.deepEqual([...err.errors()], [err.replies[1]]); return true; } diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 55355a133dd..5dae1271ecb 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -1,5 +1,6 @@ import COMMANDS from '../commands'; import RedisSocket, { RedisSocketOptions } from './socket'; +import { BasicAuth, CredentialsError, CredentialsProvider, StreamingCredentialsProvider, UnableToObtainNewCredentialsError, Disposable } from '../authx'; import RedisCommandsQueue, { CommandOptions } from './commands-queue'; import { EventEmitter } from 'node:events'; import { attachConfig, functionArgumentsPrefix, getTransformReply, scriptArgumentsPrefix } from '../commander'; @@ -42,6 +43,13 @@ export interface RedisClientOptions< * ACL password or the old "--requirepass" password */ password?: string; + + /** + * Provides credentials for authentication. Can be set directly or will be created internally + * if username/password are provided instead. If both are supplied, this credentialsProvider + * takes precedence over username/password. + */ + credentialsProvider?: CredentialsProvider; /** * Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname)) */ @@ -261,6 +269,17 @@ export default class RedisClient< parsed.password = decodeURIComponent(password); } + if (username || password) { + parsed.credentialsProvider = { + type: 'async-credentials-provider', + credentials: async () => ( + { + username: username ? decodeURIComponent(username) : undefined, + password: password ? decodeURIComponent(password) : undefined + }) + }; + } + if (pathname.length > 1) { const database = Number(pathname.substring(1)); if (isNaN(database)) { @@ -284,6 +303,8 @@ export default class RedisClient< #epoch: number; #watchEpoch?: number; + #credentialsSubscription: Disposable | null = null; + get options(): RedisClientOptions | undefined { return this._self.#options; } @@ -317,6 +338,19 @@ export default class RedisClient< } #initiateOptions(options?: RedisClientOptions): RedisClientOptions | undefined { + + // Convert username/password to credentialsProvider if no credentialsProvider is already in place + if (!options?.credentialsProvider && (options?.username || options?.password)) { + + options.credentialsProvider = { + type: 'async-credentials-provider', + credentials: async () => ({ + username: options.username, + password: options.password + }) + }; + } + if (options?.url) { const parsed = RedisClient.parseURL(options.url); if (options.socket) { @@ -345,17 +379,65 @@ export default class RedisClient< ); } - #handshake(selectedDB: number) { + /** + * @param credentials + */ + private reAuthenticate = async (credentials: BasicAuth) => { + // Re-authentication is not supported on RESP2 with PubSub active + if (!(this.isPubSubActive && !this.#options?.RESP)) { + await this.sendCommand( + parseArgs(COMMANDS.AUTH, { + username: credentials.username, + password: credentials.password ?? '' + }) + ); + } + } + + #subscribeForStreamingCredentials(cp: StreamingCredentialsProvider): Promise<[BasicAuth, Disposable]> { + return cp.subscribe({ + onNext: credentials => { + this.reAuthenticate(credentials).catch(error => { + const errorMessage = error instanceof Error ? error.message : String(error); + cp.onReAuthenticationError(new CredentialsError(errorMessage)); + }); + + }, + onError: (e: Error) => { + const errorMessage = `Error from streaming credentials provider: ${e.message}`; + cp.onReAuthenticationError(new UnableToObtainNewCredentialsError(errorMessage)); + } + }); + } + + async #handshake(selectedDB: number) { const commands = []; + const cp = this.#options?.credentialsProvider; if (this.#options?.RESP) { const hello: HelloOptions = {}; - if (this.#options.password) { - hello.AUTH = { - username: this.#options.username ?? 'default', - password: this.#options.password - }; + if (cp && cp.type === 'async-credentials-provider') { + const credentials = await cp.credentials(); + if (credentials.password) { + hello.AUTH = { + username: credentials.username ?? 'default', + password: credentials.password + }; + } + } + + if (cp && cp.type === 'streaming-credentials-provider') { + + const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) + this.#credentialsSubscription = disposable; + + if (credentials.password) { + hello.AUTH = { + username: credentials.username ?? 'default', + password: credentials.password + }; + } } if (this.#options.name) { @@ -366,13 +448,34 @@ export default class RedisClient< parseArgs(HELLO, this.#options.RESP, hello) ); } else { - if (this.#options?.username || this.#options?.password) { - commands.push( - parseArgs(COMMANDS.AUTH, { - username: this.#options.username, - password: this.#options.password ?? '' - }) - ); + + if (cp && cp.type === 'async-credentials-provider') { + + const credentials = await cp.credentials(); + + if (credentials.username || credentials.password) { + commands.push( + parseArgs(COMMANDS.AUTH, { + username: credentials.username, + password: credentials.password ?? '' + }) + ); + } + } + + if (cp && cp.type === 'streaming-credentials-provider') { + + const [credentials, disposable] = await this.#subscribeForStreamingCredentials(cp) + this.#credentialsSubscription = disposable; + + if (credentials.username || credentials.password) { + commands.push( + parseArgs(COMMANDS.AUTH, { + username: credentials.username, + password: credentials.password ?? '' + }) + ); + } } if (this.#options?.name) { @@ -396,7 +499,7 @@ export default class RedisClient< } #initiateSocket(): RedisSocket { - const socketInitiator = () => { + const socketInitiator = async () => { const promises = [], chainId = Symbol('Socket Initiator'); @@ -418,7 +521,7 @@ export default class RedisClient< ); } - const commands = this.#handshake(this.#selectedDB); + const commands = await this.#handshake(this.#selectedDB); for (let i = commands.length - 1; i >= 0; --i) { promises.push( this.#queue.addCommand(commands[i], { @@ -1000,7 +1103,9 @@ export default class RedisClient< const chainId = Symbol('Reset Chain'), promises = [this._self.#queue.reset(chainId)], selectedDB = this._self.#options?.database ?? 0; - for (const command of this._self.#handshake(selectedDB)) { + this._self.#credentialsSubscription?.dispose(); + this._self.#credentialsSubscription = null; + for (const command of (await this._self.#handshake(selectedDB))) { promises.push( this._self.#queue.addCommand(command, { chainId @@ -1051,6 +1156,8 @@ export default class RedisClient< * @deprecated use .close instead */ QUIT(): Promise { + this._self.#credentialsSubscription?.dispose(); + this._self.#credentialsSubscription = null; return this._self.#socket.quit(async () => { clearTimeout(this._self.#pingTimer); const quitPromise = this._self.#queue.addCommand(['QUIT']); @@ -1089,6 +1196,8 @@ export default class RedisClient< resolve(); }; this._self.#socket.on('data', maybeClose); + this._self.#credentialsSubscription?.dispose(); + this._self.#credentialsSubscription = null; }); } @@ -1099,6 +1208,8 @@ export default class RedisClient< clearTimeout(this._self.#pingTimer); this._self.#queue.flushAll(new DisconnectsClientError()); this._self.#socket.destroy(); + this._self.#credentialsSubscription?.dispose(); + this._self.#credentialsSubscription = null; } ref() { diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 083c9127e5b..2d561dd2e20 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -1,6 +1,7 @@ import TestUtils from '@redis/test-utils'; import { SinonSpy } from 'sinon'; import { setTimeout } from 'node:timers/promises'; +import { CredentialsProvider } from './authx'; import { Command } from './RESP/types'; import { BasicCommandParser } from './client/parser'; @@ -16,6 +17,31 @@ const DEBUG_MODE_ARGS = utils.isVersionGreaterThan([7]) ? ['--enable-debug-command', 'yes'] : []; +const asyncBasicAuthCredentialsProvider: CredentialsProvider = + { + type: 'async-credentials-provider', + credentials: async () => ({ password: 'password' }) + } as const; + +const streamingCredentialsProvider: CredentialsProvider = + { + type: 'streaming-credentials-provider', + + subscribe : (observable) => ( Promise.resolve([ + { password: 'password' }, + { + dispose: () => { + console.log('disposing credentials provider subscription'); + } + } + ])), + + onReAuthenticationError: (error) => { + console.error('re-authentication error', error); + } + + } as const; + export const GLOBAL = { SERVERS: { OPEN: { @@ -26,6 +52,18 @@ export const GLOBAL = { clientOptions: { password: 'password' } + }, + ASYNC_BASIC_AUTH: { + serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], + clientOptions: { + credentialsProvider: asyncBasicAuthCredentialsProvider + } + }, + STREAMING_AUTH: { + serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], + clientOptions: { + credentialsProvider: streamingCredentialsProvider + } } }, CLUSTERS: { diff --git a/packages/entraid/.nycrc.json b/packages/entraid/.nycrc.json new file mode 100644 index 00000000000..848af2b5a27 --- /dev/null +++ b/packages/entraid/.nycrc.json @@ -0,0 +1,10 @@ +{ + "extends": "@istanbuljs/nyc-config-typescript", + "exclude": [ + "integration-tests", + "samples", + "dist", + "**/*.spec.ts", + "lib/test-utils.ts" + ] +} diff --git a/packages/entraid/.release-it.json b/packages/entraid/.release-it.json new file mode 100644 index 00000000000..a5f3a31062e --- /dev/null +++ b/packages/entraid/.release-it.json @@ -0,0 +1,11 @@ +{ + "git": { + "tagName": "entraid@${version}", + "commitMessage": "Release ${tagName}", + "tagAnnotation": "Release ${tagName}" + }, + "npm": { + "versionArgs": ["--workspaces-update=false"], + "publishArgs": ["--access", "public"] + } +} diff --git a/packages/entraid/README.md b/packages/entraid/README.md new file mode 100644 index 00000000000..e9c7956022e --- /dev/null +++ b/packages/entraid/README.md @@ -0,0 +1,137 @@ +# @redis/entraid + +Secure token-based authentication for Redis clients using Microsoft Entra ID (formerly Azure Active Directory). + +## Features + +- Token-based authentication using Microsoft Entra ID +- Automatic token refresh before expiration +- Automatic re-authentication of all connections after token refresh +- Support for multiple authentication flows: + - Managed identities (system-assigned and user-assigned) + - Service principals (with or without certificates) + - Authorization Code with PKCE flow +- Built-in retry mechanisms for transient failures + +## Installation + +```bash +npm install @redis/client +npm install @redis/entraid +``` + +## Getting Started + +The first step to using @redis/entraid is choosing the right credentials provider for your authentication needs. The `EntraIdCredentialsProviderFactory` class provides several factory methods to create the appropriate provider: + +- `createForSystemAssignedManagedIdentity`: Use when your application runs in Azure with a system-assigned managed identity +- `createForUserAssignedManagedIdentity`: Use when your application runs in Azure with a user-assigned managed identity +- `createForClientCredentials`: Use when authenticating with a service principal using client secret +- `createForClientCredentialsWithCertificate`: Use when authenticating with a service principal using a certificate +- `createForAuthorizationCodeWithPKCE`: Use for interactive authentication flows in user applications + +## Usage Examples + +### Service Principal Authentication + +```typescript +import { createClient } from '@redis/client'; +import { EntraIdCredentialsProviderFactory } from '@redis/entraid'; + +const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({ + clientId: 'your-client-id', + clientSecret: 'your-client-secret', + authorityConfig: { + type: 'multi-tenant', + tenantId: 'your-tenant-id' + }, + tokenManagerConfig: { + expirationRefreshRatio: 0.8 // Refresh token after 80% of its lifetime + } +}); + +const client = createClient({ + url: 'redis://your-host', + credentialsProvider: provider +}); + +await client.connect(); +``` + +### System-Assigned Managed Identity + +```typescript +const provider = EntraIdCredentialsProviderFactory.createForSystemAssignedManagedIdentity({ + clientId: 'your-client-id', + tokenManagerConfig: { + expirationRefreshRatio: 0.8 + } +}); +``` + +### User-Assigned Managed Identity + +```typescript +const provider = EntraIdCredentialsProviderFactory.createForUserAssignedManagedIdentity({ + clientId: 'your-client-id', + userAssignedClientId: 'your-user-assigned-client-id', + tokenManagerConfig: { + expirationRefreshRatio: 0.8 + } +}); +``` + +## Important Limitations + +### RESP2 PUB/SUB Limitations + +When using RESP2 (Redis Serialization Protocol 2), there are important limitations with PUB/SUB: + +- **No Re-Authentication in PUB/SUB Mode**: In RESP2, once a connection enters PUB/SUB mode, the socket is blocked and cannot process out-of-band commands like AUTH. This means that connections in PUB/SUB mode cannot be re-authenticated when tokens are refreshed. +- **Connection Eviction**: As a result, PUB/SUB connections will be evicted by the Redis proxy when their tokens expire. The client will need to establish new connections with fresh tokens. + +### Transaction Safety + +When using token-based authentication, special care must be taken with Redis transactions. The token manager runs in the background and may attempt to re-authenticate connections at any time by sending AUTH commands. This can interfere with manually constructed transactions. + +#### ✅ Recommended: Use the Official Transaction API + +Always use the official transaction API provided by the client: + +```typescript +// Correct way to handle transactions +const multi = client.multi(); +multi.set('key1', 'value1'); +multi.set('key2', 'value2'); +await multi.exec(); +``` + +#### ❌ Avoid: Manual Transaction Construction + +Do not manually construct transactions by sending individual MULTI/EXEC commands: + +```typescript +// Incorrect and potentially dangerous +await client.sendCommand(['MULTI']); +await client.sendCommand(['SET', 'key1', 'value1']); +await client.sendCommand(['SET', 'key2', 'value2']); +await client.sendCommand(['EXEC']); // Risk of AUTH command being injected before EXEC +``` + +## Error Handling + +The provider includes built-in retry mechanisms for transient errors: + +```typescript +const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({ + // ... other config ... + tokenManagerConfig: { + retry: { + maxAttempts: 3, + initialDelayMs: 100, + maxDelayMs: 1000, + backoffMultiplier: 2 + } + } +}); +``` diff --git a/packages/entraid/integration-tests/entraid-integration.spec.ts b/packages/entraid/integration-tests/entraid-integration.spec.ts new file mode 100644 index 00000000000..deb1d47dec1 --- /dev/null +++ b/packages/entraid/integration-tests/entraid-integration.spec.ts @@ -0,0 +1,217 @@ +import { BasicAuth } from '@redis/client/dist/lib/authx'; +import { createClient } from '@redis/client'; +import { EntraIdCredentialsProviderFactory } from '../lib/entra-id-credentials-provider-factory'; +import { strict as assert } from 'node:assert'; +import { spy, SinonSpy } from 'sinon'; +import { randomUUID } from 'crypto'; +import { loadFromFile, RedisEndpointsConfig } from '@redis/test-utils/lib/cae-client-testing'; +import { EntraidCredentialsProvider } from '../lib/entraid-credentials-provider'; +import * as crypto from 'node:crypto'; + +describe('EntraID Integration Tests', () => { + + it('client configured with client secret should be able to authenticate/re-authenticate', async () => { + const config = await readConfigFromEnv(); + await runAuthenticationTest(() => + EntraIdCredentialsProviderFactory.createForClientCredentials({ + clientId: config.clientId, + clientSecret: config.clientSecret, + authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId }, + tokenManagerConfig: { + expirationRefreshRatio: 0.0001 + } + }) + ); + }); + + it('client configured with client certificate should be able to authenticate/re-authenticate', async () => { + const config = await readConfigFromEnv(); + await runAuthenticationTest(() => + EntraIdCredentialsProviderFactory.createForClientCredentialsWithCertificate({ + clientId: config.clientId, + certificate: convertCertsForMSAL(config.cert, config.privateKey), + authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId }, + tokenManagerConfig: { + expirationRefreshRatio: 0.0001 + } + }) + ); + }); + + it('client with system managed identity should be able to authenticate/re-authenticate', async () => { + const config = await readConfigFromEnv(); + await runAuthenticationTest(() => + EntraIdCredentialsProviderFactory.createForSystemAssignedManagedIdentity({ + clientId: config.clientId, + authorityConfig: { type: 'multi-tenant', tenantId: config.tenantId }, + tokenManagerConfig: { + expirationRefreshRatio: 0.00001 + } + }) + ); + }); + + interface TestConfig { + clientId: string; + clientSecret: string; + authority: string; + tenantId: string; + redisScopes: string; + cert: string; + privateKey: string; + userAssignedManagedId: string; + endpoints: RedisEndpointsConfig; + } + + const readConfigFromEnv = async (): Promise => { + const requiredEnvVars = { + AZURE_CLIENT_ID: process.env.AZURE_CLIENT_ID, + AZURE_CLIENT_SECRET: process.env.AZURE_CLIENT_SECRET, + AZURE_AUTHORITY: process.env.AZURE_AUTHORITY, + AZURE_TENANT_ID: process.env.AZURE_TENANT_ID, + AZURE_REDIS_SCOPES: process.env.AZURE_REDIS_SCOPES, + AZURE_CERT: process.env.AZURE_CERT, + AZURE_PRIVATE_KEY: process.env.AZURE_PRIVATE_KEY, + AZURE_USER_ASSIGNED_MANAGED_ID: process.env.AZURE_USER_ASSIGNED_MANAGED_ID, + REDIS_ENDPOINTS_CONFIG_PATH: process.env.REDIS_ENDPOINTS_CONFIG_PATH + }; + + Object.entries(requiredEnvVars).forEach(([key, value]) => { + if (value == undefined) { + throw new Error(`${key} environment variable must be set`); + } + }); + + return { + endpoints: await loadFromFile(requiredEnvVars.REDIS_ENDPOINTS_CONFIG_PATH), + clientId: requiredEnvVars.AZURE_CLIENT_ID, + clientSecret: requiredEnvVars.AZURE_CLIENT_SECRET, + authority: requiredEnvVars.AZURE_AUTHORITY, + tenantId: requiredEnvVars.AZURE_TENANT_ID, + redisScopes: requiredEnvVars.AZURE_REDIS_SCOPES, + cert: requiredEnvVars.AZURE_CERT, + privateKey: requiredEnvVars.AZURE_PRIVATE_KEY, + userAssignedManagedId: requiredEnvVars.AZURE_USER_ASSIGNED_MANAGED_ID + }; + }; + + interface TokenDetail { + token: string; + exp: number; + iat: number; + lifetime: number; + uti: string; + } + + const setupTestClient = async (credentialsProvider: EntraidCredentialsProvider) => { + const config = await readConfigFromEnv(); + const client = createClient({ + url: config.endpoints['standalone-entraid-acl'].endpoints[0], + credentialsProvider + }); + + const clientInstance = (client as any)._self; + const reAuthSpy: SinonSpy = spy(clientInstance, 'reAuthenticate'); + + return { client, reAuthSpy }; + }; + + const runClientOperations = async (client: any) => { + const startTime = Date.now(); + while (Date.now() - startTime < 1000) { + const key = randomUUID(); + await client.set(key, 'value'); + const value = await client.get(key); + assert.equal(value, 'value'); + await client.del(key); + } + }; + + const validateTokens = (reAuthSpy: SinonSpy) => { + assert(reAuthSpy.callCount >= 1, + `reAuthenticate should have been called at least once, but was called ${reAuthSpy.callCount} times`); + + const tokenDetails: TokenDetail[] = reAuthSpy.getCalls().map(call => { + const creds = call.args[0] as BasicAuth; + const tokenPayload = JSON.parse( + Buffer.from(creds.password.split('.')[1], 'base64').toString() + ); + + return { + token: creds.password, + exp: tokenPayload.exp, + iat: tokenPayload.iat, + lifetime: tokenPayload.exp - tokenPayload.iat, + uti: tokenPayload.uti + }; + }); + + // Verify unique tokens + const uniqueTokens = new Set(tokenDetails.map(detail => detail.token)); + assert.equal( + uniqueTokens.size, + reAuthSpy.callCount, + `Expected ${reAuthSpy.callCount} different tokens, but got ${uniqueTokens.size} unique tokens` + ); + + // Verify all tokens are not cached (i.e. have the same lifetime) + const uniqueLifetimes = new Set(tokenDetails.map(detail => detail.lifetime)); + assert.equal( + uniqueLifetimes.size, + 1, + `Expected all tokens to have the same lifetime, but found ${uniqueLifetimes.size} different lifetimes: ${[uniqueLifetimes].join(', ')} seconds` + ); + + // Verify that all tokens have different uti (unique token identifier) + const uniqueUti = new Set(tokenDetails.map(detail => detail.uti)); + assert.equal( + uniqueUti.size, + reAuthSpy.callCount, + `Expected all tokens to have different uti, but found ${uniqueUti.size} different uti in: ${[uniqueUti].join(', ')}` + ); + }; + + const runAuthenticationTest = async (setupCredentialsProvider: () => any) => { + const { client, reAuthSpy } = await setupTestClient(setupCredentialsProvider()); + + try { + await client.connect(); + await runClientOperations(client); + validateTokens(reAuthSpy); + } finally { + await client.destroy(); + } + }; + +}); + +function getCertificate(certBase64) { + try { + const decodedCert = Buffer.from(certBase64, 'base64'); + const cert = new crypto.X509Certificate(decodedCert); + return cert; + } catch (error) { + console.error('Error parsing certificate:', error); + throw error; + } +} + +function getCertificateThumbprint(certBase64) { + const cert = getCertificate(certBase64); + return cert.fingerprint.replace(/:/g, ''); +} + +function convertCertsForMSAL(certBase64, privateKeyBase64) { + const thumbprint = getCertificateThumbprint(certBase64); + + const privateKeyPEM = `-----BEGIN PRIVATE KEY-----\n${privateKeyBase64}\n-----END PRIVATE KEY-----`; + + return { + thumbprint: thumbprint, + privateKey: privateKeyPEM, + x5c: certBase64 + } + +} + + diff --git a/packages/entraid/lib/entra-id-credentials-provider-factory.ts b/packages/entraid/lib/entra-id-credentials-provider-factory.ts new file mode 100644 index 00000000000..0f89be8039b --- /dev/null +++ b/packages/entraid/lib/entra-id-credentials-provider-factory.ts @@ -0,0 +1,371 @@ +import { NetworkError } from '@azure/msal-common'; +import { + LogLevel, + ManagedIdentityApplication, + ManagedIdentityConfiguration, + AuthenticationResult, + PublicClientApplication, + ConfidentialClientApplication, AuthorizationUrlRequest, AuthorizationCodeRequest, CryptoProvider, Configuration, NodeAuthOptions, AccountInfo +} from '@azure/msal-node'; +import { RetryPolicy, TokenManager, TokenManagerConfig, ReAuthenticationError } from '@redis/client/dist/lib/authx'; +import { EntraidCredentialsProvider } from './entraid-credentials-provider'; +import { MSALIdentityProvider } from './msal-identity-provider'; + +/** + * This class is used to create credentials providers for different types of authentication flows. + */ +export class EntraIdCredentialsProviderFactory { + + /** + * This method is used to create a ManagedIdentityProvider for both system-assigned and user-assigned managed identities. + * + * @param params + * @param userAssignedClientId For user-assigned managed identities, the developer needs to pass either the client ID, + * full resource identifier, or the object ID of the managed identity when creating ManagedIdentityApplication. + * + */ + public static createManagedIdentityProvider( + params: CredentialParams, userAssignedClientId?: string + ): EntraidCredentialsProvider { + const config: ManagedIdentityConfiguration = { + // For user-assigned identity, include the client ID + ...(userAssignedClientId && { + managedIdentityIdParams: { + userAssignedClientId + } + }), + system: { + loggerOptions + } + }; + + const client = new ManagedIdentityApplication(config); + + const idp = new MSALIdentityProvider( + () => client.acquireToken({ + resource: params.scopes?.[0] ?? REDIS_SCOPE, + forceRefresh: true + }).then(x => x === null ? Promise.reject('Token is null') : x) + ); + + return new EntraidCredentialsProvider( + new TokenManager(idp, params.tokenManagerConfig), + idp, + { onReAuthenticationError: params.onReAuthenticationError, credentialsMapper: OID_CREDENTIALS_MAPPER } + ); + } + + /** + * This method is used to create a credentials provider for system-assigned managed identities. + * @param params + */ + static createForSystemAssignedManagedIdentity( + params: CredentialParams + ): EntraidCredentialsProvider { + return this.createManagedIdentityProvider(params); + } + + /** + * This method is used to create a credentials provider for user-assigned managed identities. + * It will include the client ID as the userAssignedClientId in the ManagedIdentityConfiguration. + * @param params + */ + static createForUserAssignedManagedIdentity( + params: CredentialParams & { userAssignedClientId: string } + ): EntraidCredentialsProvider { + return this.createManagedIdentityProvider(params, params.userAssignedClientId); + } + + static #createForClientCredentials( + authConfig: NodeAuthOptions, + params: CredentialParams + ): EntraidCredentialsProvider { + const config: Configuration = { + auth: { + ...authConfig, + authority: this.getAuthority(params.authorityConfig ?? { type: 'default' }) + }, + system: { + loggerOptions + } + }; + + const client = new ConfidentialClientApplication(config); + + const idp = new MSALIdentityProvider( + () => client.acquireTokenByClientCredential({ + skipCache: true, + scopes: params.scopes ?? [REDIS_SCOPE_DEFAULT] + }).then(x => x === null ? Promise.reject('Token is null') : x) + ); + + return new EntraidCredentialsProvider(new TokenManager(idp, params.tokenManagerConfig), idp, + { + onReAuthenticationError: params.onReAuthenticationError, + credentialsMapper: OID_CREDENTIALS_MAPPER + }); + } + + /** + * This method is used to create a credentials provider for service principals using certificate. + * @param params + */ + static createForClientCredentialsWithCertificate( + params: ClientCredentialsWithCertificateParams + ): EntraidCredentialsProvider { + return this.#createForClientCredentials( + { + clientId: params.clientId, + clientCertificate: params.certificate + }, + params + ); + } + + /** + * This method is used to create a credentials provider for service principals using client secret. + * @param params + */ + static createForClientCredentials( + params: ClientSecretCredentialsParams + ): EntraidCredentialsProvider { + return this.#createForClientCredentials( + { + clientId: params.clientId, + clientSecret: params.clientSecret + }, + params + ); + } + + /** + * This method is used to create a credentials provider for the Authorization Code Flow with PKCE. + * @param params + */ + static createForAuthorizationCodeWithPKCE( + params: AuthCodePKCEParams + ): { + getPKCECodes: () => Promise<{ + verifier: string; + challenge: string; + challengeMethod: string; + }>; + getAuthCodeUrl: ( + pkceCodes: { challenge: string; challengeMethod: string } + ) => Promise; + createCredentialsProvider: ( + params: PKCEParams + ) => EntraidCredentialsProvider; + } { + + const requiredScopes = ['user.read', 'offline_access']; + const scopes = [...new Set([...(params.scopes || []), ...requiredScopes])]; + + const authFlow = AuthCodeFlowHelper.create({ + clientId: params.clientId, + redirectUri: params.redirectUri, + scopes: scopes, + authorityConfig: params.authorityConfig + }); + + return { + getPKCECodes: AuthCodeFlowHelper.generatePKCE, + getAuthCodeUrl: (pkceCodes) => authFlow.getAuthCodeUrl(pkceCodes), + createCredentialsProvider: (pkceParams) => { + + // This is used to store the initial credentials account to be used + // for silent token acquisition after the initial token acquisition. + let initialCredentialsAccount: AccountInfo | null = null; + + const idp = new MSALIdentityProvider( + async () => { + if (!initialCredentialsAccount) { + let authResult = await authFlow.acquireTokenByCode(pkceParams); + initialCredentialsAccount = authResult.account; + return authResult; + } else { + return authFlow.client.acquireTokenSilent({ + forceRefresh: true, + account: initialCredentialsAccount, + scopes + }); + } + + } + ); + const tm = new TokenManager(idp, params.tokenManagerConfig); + return new EntraidCredentialsProvider(tm, idp, { onReAuthenticationError: params.onReAuthenticationError }); + } + }; + } + + static getAuthority(config: AuthorityConfig): string { + switch (config.type) { + case 'multi-tenant': + return `https://login.microsoftonline.com/${config.tenantId}`; + case 'custom': + return config.authorityUrl; + case 'default': + return 'https://login.microsoftonline.com/common'; + default: + throw new Error('Invalid authority configuration'); + } + } + +} + +const REDIS_SCOPE_DEFAULT = 'https://redis.azure.com/.default'; +const REDIS_SCOPE = 'https://redis.azure.com' + +export type AuthorityConfig = + | { type: 'multi-tenant'; tenantId: string } + | { type: 'custom'; authorityUrl: string } + | { type: 'default' }; + +export type PKCEParams = { + code: string; + verifier: string; + clientInfo?: string; +} + +export type CredentialParams = { + clientId: string; + scopes?: string[]; + authorityConfig?: AuthorityConfig; + + tokenManagerConfig: TokenManagerConfig + onReAuthenticationError?: (error: ReAuthenticationError) => void; +} + +export type AuthCodePKCEParams = CredentialParams & { + redirectUri: string; +}; + +export type ClientSecretCredentialsParams = CredentialParams & { + clientSecret: string; +}; + +export type ClientCredentialsWithCertificateParams = CredentialParams & { + certificate: { + thumbprint: string; + privateKey: string; + x5c?: string; + }; +}; + +const loggerOptions = { + loggerCallback(loglevel: LogLevel, message: string, containsPii: boolean) { + if (!containsPii) console.log(message); + }, + piiLoggingEnabled: false, + logLevel: LogLevel.Error +} + +/** + * The most important part of the RetryPolicy is the `isRetryable` function. This function is used to determine if a request should be retried based + * on the error returned from the identity provider. The default for is to retry on network errors only. + */ +export const DEFAULT_RETRY_POLICY: RetryPolicy = { + // currently only retry on network errors + isRetryable: (error: unknown) => error instanceof NetworkError, + maxAttempts: 10, + initialDelayMs: 100, + maxDelayMs: 100000, + backoffMultiplier: 2, + jitterPercentage: 0.1 + +}; + +export const DEFAULT_TOKEN_MANAGER_CONFIG: TokenManagerConfig = { + retry: DEFAULT_RETRY_POLICY, + expirationRefreshRatio: 0.7 // Refresh token when 70% of the token has expired +} + +/** + * This class is used to help with the Authorization Code Flow with PKCE. + * It provides methods to generate PKCE codes, get the authorization URL, and create the credential provider. + */ +export class AuthCodeFlowHelper { + private constructor( + readonly client: PublicClientApplication, + readonly scopes: string[], + readonly redirectUri: string + ) {} + + async getAuthCodeUrl(pkceCodes: { + challenge: string; + challengeMethod: string; + }): Promise { + const authCodeUrlParameters: AuthorizationUrlRequest = { + scopes: this.scopes, + redirectUri: this.redirectUri, + codeChallenge: pkceCodes.challenge, + codeChallengeMethod: pkceCodes.challengeMethod + }; + + return this.client.getAuthCodeUrl(authCodeUrlParameters); + } + + async acquireTokenByCode(params: PKCEParams): Promise { + const tokenRequest: AuthorizationCodeRequest = { + code: params.code, + scopes: this.scopes, + redirectUri: this.redirectUri, + codeVerifier: params.verifier, + clientInfo: params.clientInfo + }; + + return this.client.acquireTokenByCode(tokenRequest); + } + + static async generatePKCE(): Promise<{ + verifier: string; + challenge: string; + challengeMethod: string; + }> { + const cryptoProvider = new CryptoProvider(); + const { verifier, challenge } = await cryptoProvider.generatePkceCodes(); + return { + verifier, + challenge, + challengeMethod: 'S256' + }; + } + + static create(params: { + clientId: string; + redirectUri: string; + scopes?: string[]; + authorityConfig?: AuthorityConfig; + }): AuthCodeFlowHelper { + const config = { + auth: { + clientId: params.clientId, + authority: EntraIdCredentialsProviderFactory.getAuthority(params.authorityConfig ?? { type: 'default' }) + }, + system: { + loggerOptions + } + }; + + return new AuthCodeFlowHelper( + new PublicClientApplication(config), + params.scopes ?? ['user.read'], + params.redirectUri + ); + } +} + +const OID_CREDENTIALS_MAPPER = (token: AuthenticationResult) => { + + // Client credentials flow is app-only authentication (no user context), + // so only access token is provided without user-specific claims (uniqueId, idToken, ...) + // this means that we need to extract the oid from the access token manually + const accessToken = JSON.parse(Buffer.from(token.accessToken.split('.')[1], 'base64').toString()); + + return ({ + username: accessToken.oid, + password: token.accessToken + }) + +} diff --git a/packages/entraid/lib/entraid-credentials-provider.spec.ts b/packages/entraid/lib/entraid-credentials-provider.spec.ts new file mode 100644 index 00000000000..1bdf4e9b65f --- /dev/null +++ b/packages/entraid/lib/entraid-credentials-provider.spec.ts @@ -0,0 +1,199 @@ +import { AuthenticationResult } from '@azure/msal-node'; +import { IdentityProvider, TokenManager, TokenResponse, BasicAuth } from '@redis/client/dist/lib/authx'; +import { EntraidCredentialsProvider } from './entraid-credentials-provider'; +import { setTimeout } from 'timers/promises'; +import { strict as assert } from 'node:assert'; +import { GLOBAL, testUtils } from './test-utils' + + +describe('EntraID authentication in cluster mode', () => { + + testUtils.testWithCluster('sendCommand', async cluster => { + assert.equal( + await cluster.sendCommand(undefined, true, ['PING']), + 'PONG' + ); + }, GLOBAL.CLUSTERS.PASSWORD_WITH_REPLICAS); +}) + +describe('EntraID CredentialsProvider Subscription Behavior', () => { + + it('should properly handle token refresh sequence for multiple subscribers', async () => { + const networkDelay = 20; + const tokenTTL = 100; + const refreshRatio = 0.5; // Refresh at 50% of TTL + + const idp = new SequenceEntraIDProvider(tokenTTL, networkDelay); + const tokenManager = new TokenManager(idp, { + expirationRefreshRatio: refreshRatio + }); + const entraid = new EntraidCredentialsProvider(tokenManager, idp); + + // Create two initial subscribers + const subscriber1 = new TestSubscriber('subscriber1'); + const subscriber2 = new TestSubscriber('subscriber2'); + + assert.equal(entraid.hasActiveSubscriptions(), false, 'There should be no active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 0, 'There should be 0 subscriptions'); + + // Start the first two subscriptions almost simultaneously + const [sub1Initial, sub2Initial] = await Promise.all([ + entraid.subscribe(subscriber1), + entraid.subscribe(subscriber2)] + ); + + assertCredentials(sub1Initial[0], 'initial-token', 'Subscriber 1 should receive initial token'); + assertCredentials(sub2Initial[0], 'initial-token', 'Subscriber 2 should receive initial token'); + + assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 2, 'There should be 2 subscriptions'); + + // add a third subscriber after a very short delay + const subscriber3 = new TestSubscriber('subscriber3'); + await setTimeout(1); + const sub3Initial = await entraid.subscribe(subscriber3) + + assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 3, 'There should be 3 subscriptions'); + + // make sure the third subscriber gets the initial token as well + assertCredentials(sub3Initial[0], 'initial-token', 'Subscriber 3 should receive initial token'); + + // Wait for first refresh (50% of TTL + network delay + small buffer) + await setTimeout((tokenTTL * refreshRatio) + networkDelay + 15); + + // All 3 subscribers should receive refresh-token-1 + assertCredentials(subscriber1.credentials[0], 'refresh-token-1', 'Subscriber 1 should receive first refresh token'); + assertCredentials(subscriber2.credentials[0], 'refresh-token-1', 'Subscriber 2 should receive first refresh token'); + assertCredentials(subscriber3.credentials[0], 'refresh-token-1', 'Subscriber 3 should receive first refresh token'); + + // Add a late subscriber - should immediately get refresh-token-1 + const subscriber4 = new TestSubscriber('subscriber4'); + const sub4Initial = await entraid.subscribe(subscriber4); + + assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 4, 'There should be 4 subscriptions'); + + assertCredentials(sub4Initial[0], 'refresh-token-1', 'Late subscriber should receive refresh-token-1'); + + // Wait for second refresh + await setTimeout((tokenTTL * refreshRatio) + networkDelay + 15); + + assertCredentials(subscriber1.credentials[1], 'refresh-token-2', 'Subscriber 1 should receive second refresh token'); + assertCredentials(subscriber2.credentials[1], 'refresh-token-2', 'Subscriber 2 should receive second refresh token'); + assertCredentials(subscriber3.credentials[1], 'refresh-token-2', 'Subscriber 3 should receive second refresh token'); + + assertCredentials(subscriber4.credentials[0], 'refresh-token-2', 'Subscriber 4 should receive second refresh token'); + + // Verify refreshes happen after minimum expected time + const minimumRefreshInterval = tokenTTL * 0.4; // 40% of TTL as safety margin + + verifyRefreshTiming(subscriber1, minimumRefreshInterval); + verifyRefreshTiming(subscriber2, minimumRefreshInterval); + verifyRefreshTiming(subscriber3, minimumRefreshInterval); + verifyRefreshTiming(subscriber4, minimumRefreshInterval); + + // Cleanup + + assert.equal(tokenManager.isRunning(), true); + sub1Initial[1].dispose(); + sub2Initial[1].dispose(); + sub3Initial[1].dispose(); + assert.equal(entraid.hasActiveSubscriptions(), true, 'There should be active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 1, 'There should be 1 subscriptions'); + sub4Initial[1].dispose(); + assert.equal(entraid.hasActiveSubscriptions(), false, 'There should be no active subscriptions'); + assert.equal(entraid.getSubscriptionsCount(), 0, 'There should be 0 subscriptions'); + assert.equal(tokenManager.isRunning(), false) + }); + + const verifyRefreshTiming = ( + subscriber: TestSubscriber, + expectedMinimumInterval: number, + message?: string + ) => { + const intervals = []; + for (let i = 1; i < subscriber.timestamps.length; i++) { + intervals.push(subscriber.timestamps[i] - subscriber.timestamps[i - 1]); + } + + intervals.forEach((interval, index) => { + assert.ok( + interval > expectedMinimumInterval, + message || `Refresh ${index + 1} for ${subscriber.name} should happen after minimum interval of ${expectedMinimumInterval}ms` + ); + }); + }; + + class SequenceEntraIDProvider implements IdentityProvider { + private currentIndex = 0; + + constructor( + private readonly tokenTTL: number = 100, + private tokenDeliveryDelayMs: number = 0, + private readonly tokenSequence: AuthenticationResult[] = [ + { + accessToken: 'initial-token', + uniqueId: 'test-user' + } as AuthenticationResult, + { + accessToken: 'refresh-token-1', + uniqueId: 'test-user' + } as AuthenticationResult, + { + accessToken: 'refresh-token-2', + uniqueId: 'test-user' + } as AuthenticationResult + ] + ) {} + + setTokenDeliveryDelay(delayMs: number): void { + this.tokenDeliveryDelayMs = delayMs; + } + + async requestToken(): Promise> { + if (this.tokenDeliveryDelayMs > 0) { + await setTimeout(this.tokenDeliveryDelayMs); + } + + if (this.currentIndex >= this.tokenSequence.length) { + throw new Error('No more tokens in sequence'); + } + + return { + token: this.tokenSequence[this.currentIndex++], + ttlMs: this.tokenTTL + }; + } + } + + class TestSubscriber { + public readonly credentials: Array = []; + public readonly errors: Error[] = []; + public readonly timestamps: number[] = []; + + constructor(public readonly name: string = 'unnamed') {} + + onNext = (creds: BasicAuth) => { + this.credentials.push(creds); + this.timestamps.push(Date.now()); + } + + onError = (error: Error) => { + this.errors.push(error); + } + } + + /** + * Assert that the actual credentials match the expected token + * @param actual + * @param expectedToken + * @param message + */ + const assertCredentials = (actual: BasicAuth, expectedToken: string, message: string) => { + assert.deepEqual(actual, { + username: 'test-user', + password: expectedToken + }, message); + }; +}); \ No newline at end of file diff --git a/packages/entraid/lib/entraid-credentials-provider.ts b/packages/entraid/lib/entraid-credentials-provider.ts new file mode 100644 index 00000000000..115d6dbff3a --- /dev/null +++ b/packages/entraid/lib/entraid-credentials-provider.ts @@ -0,0 +1,140 @@ +import { AuthenticationResult } from '@azure/msal-common/node'; +import { + BasicAuth, StreamingCredentialsProvider, IdentityProvider, TokenManager, + ReAuthenticationError, StreamingCredentialsListener, IDPError, Token, Disposable +} from '@redis/client/dist/lib/authx'; + +/** + * A streaming credentials provider that uses the Entraid identity provider to provide credentials. + * Please use one of the factory functions in `entraid-credetfactories.ts` to create an instance of this class for the different + * type of authentication flows. + */ +export class EntraidCredentialsProvider implements StreamingCredentialsProvider { + readonly type = 'streaming-credentials-provider'; + + readonly #listeners: Set> = new Set(); + + #tokenManagerDisposable: Disposable | null = null; + #isStarting: boolean = false; + + #pendingSubscribers: Array<{ + resolve: (value: [BasicAuth, Disposable]) => void; + reject: (error: Error) => void; + pendingListener: StreamingCredentialsListener; + }> = []; + + constructor( + public readonly tokenManager: TokenManager, + public readonly idp: IdentityProvider, + private readonly options: { + onReAuthenticationError?: (error: ReAuthenticationError) => void; + credentialsMapper?: (token: AuthenticationResult) => BasicAuth; + onRetryableError?: (error: string) => void; + } = {} + ) { + this.onReAuthenticationError = options.onReAuthenticationError ?? DEFAULT_ERROR_HANDLER; + this.#credentialsMapper = options.credentialsMapper ?? DEFAULT_CREDENTIALS_MAPPER; + } + + async subscribe( + listener: StreamingCredentialsListener + ): Promise<[BasicAuth, Disposable]> { + + const currentToken = this.tokenManager.getCurrentToken(); + + if (currentToken) { + return [this.#credentialsMapper(currentToken.value), this.#createDisposable(listener)]; + } + + if (this.#isStarting) { + return new Promise((resolve, reject) => { + this.#pendingSubscribers.push({ resolve, reject, pendingListener: listener }); + }); + } + + this.#isStarting = true; + try { + const initialToken = await this.#startTokenManagerAndObtainInitialToken(); + + this.#pendingSubscribers.forEach(({ resolve, pendingListener }) => { + resolve([this.#credentialsMapper(initialToken.value), this.#createDisposable(pendingListener)]); + }); + this.#pendingSubscribers = []; + + return [this.#credentialsMapper(initialToken.value), this.#createDisposable(listener)]; + } finally { + this.#isStarting = false; + } + } + + onReAuthenticationError: (error: ReAuthenticationError) => void; + + #credentialsMapper: (token: AuthenticationResult) => BasicAuth; + + #createTokenManagerListener(subscribers: Set>) { + return { + onError: (error: IDPError): void => { + if (!error.isRetryable) { + subscribers.forEach(listener => listener.onError(error)); + } else { + this.options.onRetryableError?.(error.message); + } + }, + onNext: (token: { value: AuthenticationResult }): void => { + const credentials = this.#credentialsMapper(token.value); + subscribers.forEach(listener => listener.onNext(credentials)); + } + }; + } + + #createDisposable(listener: StreamingCredentialsListener): Disposable { + this.#listeners.add(listener); + + return { + dispose: () => { + this.#listeners.delete(listener); + if (this.#listeners.size === 0 && this.#tokenManagerDisposable) { + this.#tokenManagerDisposable.dispose(); + this.#tokenManagerDisposable = null; + } + } + }; + } + + async #startTokenManagerAndObtainInitialToken(): Promise> { + const initialResponse = await this.idp.requestToken(); + const token = this.tokenManager.wrapAndSetCurrentToken(initialResponse.token, initialResponse.ttlMs); + + this.#tokenManagerDisposable = this.tokenManager.start( + this.#createTokenManagerListener(this.#listeners), + this.tokenManager.calculateRefreshTime(token) + ); + return token; + } + + public hasActiveSubscriptions(): boolean { + return this.#tokenManagerDisposable !== null && this.#listeners.size > 0; + } + + public getSubscriptionsCount(): number { + return this.#listeners.size; + } + + public getTokenManager() { + return this.tokenManager; + } + + public getCurrentCredentials(): BasicAuth | null { + const currentToken = this.tokenManager.getCurrentToken(); + return currentToken ? this.#credentialsMapper(currentToken.value) : null; + } + +} + +const DEFAULT_CREDENTIALS_MAPPER = (token: AuthenticationResult): BasicAuth => ({ + username: token.uniqueId, + password: token.accessToken +}); + +const DEFAULT_ERROR_HANDLER = (error: ReAuthenticationError) => + console.error('ReAuthenticationError', error); \ No newline at end of file diff --git a/packages/entraid/lib/index.ts b/packages/entraid/lib/index.ts new file mode 100644 index 00000000000..4873c9935c5 --- /dev/null +++ b/packages/entraid/lib/index.ts @@ -0,0 +1,3 @@ +export * from './entra-id-credentials-provider-factory'; +export * from './entraid-credentials-provider'; +export * from './msal-identity-provider'; \ No newline at end of file diff --git a/packages/entraid/lib/msal-identity-provider.ts b/packages/entraid/lib/msal-identity-provider.ts new file mode 100644 index 00000000000..59b38d18ec6 --- /dev/null +++ b/packages/entraid/lib/msal-identity-provider.ts @@ -0,0 +1,31 @@ +import { + AuthenticationResult +} from '@azure/msal-node'; +import { IdentityProvider, TokenResponse } from '@redis/client/dist/lib/authx'; + +export class MSALIdentityProvider implements IdentityProvider { + private readonly getToken: () => Promise; + + constructor(getToken: () => Promise) { + this.getToken = getToken; + } + + async requestToken(): Promise> { + try { + const result = await this.getToken(); + + if (!result?.accessToken || !result?.expiresOn) { + throw new Error('Invalid token response'); + } + return { + token: result, + ttlMs: result.expiresOn.getTime() - Date.now() + }; + } catch (error) { + throw error; + } + } + +} + + diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts new file mode 100644 index 00000000000..eecec2d4d6d --- /dev/null +++ b/packages/entraid/lib/test-utils.ts @@ -0,0 +1,46 @@ +import { AuthenticationResult } from '@azure/msal-node'; +import { IdentityProvider, StreamingCredentialsProvider, TokenManager, TokenResponse } from '@redis/client/dist/lib/authx'; +import TestUtils from '@redis/test-utils'; +import { EntraidCredentialsProvider } from './entraid-credentials-provider'; + +export const testUtils = new TestUtils({ + dockerImageName: 'redis/redis-stack', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '7.4.0-v1' +}); + +const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? + ['--enable-debug-command', 'yes'] : + []; + +const idp: IdentityProvider = { + requestToken(): Promise> { + // @ts-ignore + return Promise.resolve({ + ttlMs: 100000, + token: { + accessToken: 'password' + } + }) + } +} + +const tokenManager = new TokenManager(idp, { expirationRefreshRatio: 0.8 }); +const entraIdCredentialsProvider: StreamingCredentialsProvider = new EntraidCredentialsProvider(tokenManager, idp) + +const PASSWORD_WITH_REPLICAS = { + serverArguments: ['--requirepass', 'password', ...DEBUG_MODE_ARGS], + numberOfMasters: 2, + numberOfReplicas: 1, + clusterConfiguration: { + defaults: { + credentialsProvider: entraIdCredentialsProvider + } + } +} + +export const GLOBAL = { + CLUSTERS: { + PASSWORD_WITH_REPLICAS + } +} diff --git a/packages/entraid/package.json b/packages/entraid/package.json new file mode 100644 index 00000000000..571d78c0417 --- /dev/null +++ b/packages/entraid/package.json @@ -0,0 +1,47 @@ +{ + "name": "@redis/entraid", + "version": "5.0.0-next.5", + "license": "MIT", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist/", + "!dist/tsconfig.tsbuildinfo" + ], + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && tsc", + "start:auth-pkce": "tsx --tsconfig tsconfig.samples.json ./samples/auth-code-pkce/index.ts", + "test-integration": "mocha -r tsx --tsconfig tsconfig.integration-tests.json './integration-tests/**/*.spec.ts'", + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" + }, + "dependencies": { + "@azure/msal-node": "^2.16.1" + }, + "peerDependencies": { + "@redis/client": "^5.0.0-next.5" + }, + "devDependencies": { + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", + "@types/node": "^22.9.0", + "dotenv": "^16.3.1", + "express": "^4.21.1", + "express-session": "^1.18.1", + "@redis/test-utils": "*" + }, + "engines": { + "node": ">= 18" + }, + "repository": { + "type": "git", + "url": "git://github.com/redis/node-redis.git" + }, + "bugs": { + "url": "https://github.com/redis/node-redis/issues" + }, + "homepage": "https://github.com/redis/node-redis/tree/master/packages/entraid", + "keywords": [ + "redis" + ] +} diff --git a/packages/entraid/samples/auth-code-pkce/index.ts b/packages/entraid/samples/auth-code-pkce/index.ts new file mode 100644 index 00000000000..25429269c44 --- /dev/null +++ b/packages/entraid/samples/auth-code-pkce/index.ts @@ -0,0 +1,153 @@ +import express, { Request, Response } from 'express'; +import session from 'express-session'; +import dotenv from 'dotenv'; +import { DEFAULT_TOKEN_MANAGER_CONFIG, EntraIdCredentialsProviderFactory } from '../../lib/entra-id-credentials-provider-factory'; + +dotenv.config(); + +if (!process.env.SESSION_SECRET) { + throw new Error('SESSION_SECRET environment variable must be set'); +} + +interface PKCESession extends session.Session { + pkceCodes?: { + verifier: string; + challenge: string; + challengeMethod: string; + }; +} + +interface AuthRequest extends Request { + session: PKCESession; +} + +const app = express(); + +const sessionConfig = { + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV === 'production', // Only use secure in production + httpOnly: true, + sameSite: 'lax', + maxAge: 3600000 // 1 hour + } +} as const; + +app.use(session(sessionConfig)); + +if (!process.env.MSAL_CLIENT_ID || !process.env.MSAL_TENANT_ID) { + throw new Error('MSAL_CLIENT_ID and MSAL_TENANT_ID environment variables must be set'); +} + +// Initialize MSAL provider with authorization code PKCE flow +const { + getPKCECodes, + createCredentialsProvider, + getAuthCodeUrl +} = EntraIdCredentialsProviderFactory.createForAuthorizationCodeWithPKCE({ + clientId: process.env.MSAL_CLIENT_ID, + redirectUri: process.env.REDIRECT_URI || 'http://localhost:3000/redirect', + authorityConfig: { type: 'multi-tenant', tenantId: process.env.MSAL_TENANT_ID }, + tokenManagerConfig: DEFAULT_TOKEN_MANAGER_CONFIG +}); + +app.get('/login', async (req: AuthRequest, res: Response) => { + try { + // Generate PKCE Codes before starting the authorization flow + const pkceCodes = await getPKCECodes(); + + // Store PKCE codes in session + req.session.pkceCodes = pkceCodes + + await new Promise((resolve, reject) => { + req.session.save((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + const authUrl = await getAuthCodeUrl({ + challenge: pkceCodes.challenge, + challengeMethod: pkceCodes.challengeMethod + }); + + res.redirect(authUrl); + } catch (error) { + console.error('Login flow failed:', error); + res.status(500).send('Authentication failed'); + } +}); + +app.get('/redirect', async (req: AuthRequest, res: Response) => { + try { + + // The authorization code is in req.query.code + const { code, client_info } = req.query; + const { pkceCodes } = req.session; + + if (!pkceCodes) { + console.error('Session state:', { + hasSession: !!req.session, + sessionID: req.sessionID, + pkceCodes: req.session.pkceCodes + }); + return res.status(400).send('PKCE codes not found in session'); + } + + // Check both possible error scenarios + if (req.query.error) { + console.error('OAuth error:', req.query.error, req.query.error_description); + return res.status(400).send(`OAuth error: ${req.query.error_description || req.query.error}`); + } + + if (!code) { + console.error('Missing authorization code. Query parameters received:', req.query); + return res.status(400).send('Authorization code not found in request. Query params: ' + JSON.stringify(req.query)); + } + + // Configure with the received code + const entraidCredentialsProvider = createCredentialsProvider( + { + code: code as string, + verifier: pkceCodes.verifier, + clientInfo: client_info as string | undefined + }, + ); + + const initialCredentials = entraidCredentialsProvider.subscribe({ + onNext: (token) => { + console.log('Token acquired:', token); + }, + onError: (error) => { + console.error('Token acquisition failed:', error); + } + }); + + const [credentials] = await initialCredentials; + + console.log('Credentials acquired:', credentials) + + // Clear sensitive data + delete req.session.pkceCodes; + + await new Promise((resolve, reject) => { + req.session.save((err) => { + if (err) reject(err); + else resolve(); + }); + }); + + res.json({ message: 'Authentication successful' }); + } catch (error) { + console.error('Token acquisition failed:', error); + res.status(500).send('Failed to acquire token'); + } +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + console.log(`Login URL: http://localhost:${PORT}/login`); +}); \ No newline at end of file diff --git a/packages/entraid/tsconfig.integration-tests.json b/packages/entraid/tsconfig.integration-tests.json new file mode 100644 index 00000000000..5d15f4f2753 --- /dev/null +++ b/packages/entraid/tsconfig.integration-tests.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "./integration-tests/**/*.ts", + "./lib/**/*.ts" + ], + "compilerOptions": { + "noEmit": true + }, +} \ No newline at end of file diff --git a/packages/entraid/tsconfig.json b/packages/entraid/tsconfig.json new file mode 100644 index 00000000000..414dc1fe755 --- /dev/null +++ b/packages/entraid/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist" + }, + "include": [ + "./lib/**/*.ts" + ], + "exclude": [ + "./lib/**/*.spec.ts", + "./lib/test-util.ts", + ], + "typedocOptions": { + "entryPoints": [ + "./lib" + ], + "entryPointStrategy": "expand", + "out": "../../documentation/entraid" + } +} diff --git a/packages/entraid/tsconfig.samples.json b/packages/entraid/tsconfig.samples.json new file mode 100644 index 00000000000..0eb936369ff --- /dev/null +++ b/packages/entraid/tsconfig.samples.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "./samples/**/*.ts", + "./lib/**/*.ts" + ], + "compilerOptions": { + "noEmit": true + } +} \ No newline at end of file diff --git a/packages/test-utils/lib/cae-client-testing.ts b/packages/test-utils/lib/cae-client-testing.ts new file mode 100644 index 00000000000..92b846dd37e --- /dev/null +++ b/packages/test-utils/lib/cae-client-testing.ts @@ -0,0 +1,30 @@ +import { readFile } from 'node:fs/promises'; + +interface RawRedisEndpoint { + username?: string; + password?: string; + tls: boolean; + endpoints: string[]; +} + +export type RedisEndpointsConfig = Record; + +export function loadFromJson(jsonString: string): RedisEndpointsConfig { + try { + return JSON.parse(jsonString) as RedisEndpointsConfig; + } catch (error) { + throw new Error(`Invalid JSON configuration: ${error}`); + } +} + +export async function loadFromFile(path: string): Promise { + try { + const configFile = await readFile(path, 'utf-8'); + return loadFromJson(configFile); + } catch (error) { + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { + throw new Error(`Config file not found at path: ${path}`); + } + throw error; + } +} \ No newline at end of file diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index a1cb63eb7bf..bfb66603750 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -1,3 +1,4 @@ +import { RedisClusterClientOptions } from '@redis/client/dist/lib/cluster'; import { createConnection } from 'node:net'; import { once } from 'node:events'; import { createClient } from '@redis/client/index'; @@ -102,7 +103,8 @@ async function spawnRedisClusterNodeDockers( dockersConfig: RedisClusterDockersConfig, serverArguments: Array, fromSlot: number, - toSlot: number + toSlot: number, + clientConfig?: Partial ) { const range: Array = []; for (let i = fromSlot; i < toSlot; i++) { @@ -111,7 +113,8 @@ async function spawnRedisClusterNodeDockers( const master = await spawnRedisClusterNodeDocker( dockersConfig, - serverArguments + serverArguments, + clientConfig ); await master.client.clusterAddSlots(range); @@ -127,7 +130,13 @@ async function spawnRedisClusterNodeDockers( 'yes', '--cluster-node-timeout', '5000' - ]).then(async replica => { + ], clientConfig).then(async replica => { + + const requirePassIndex = serverArguments.findIndex((x)=>x==='--requirepass'); + if(requirePassIndex!==-1) { + const password = serverArguments[requirePassIndex+1]; + await replica.client.configSet({'masterauth': password}) + } await replica.client.clusterMeet('127.0.0.1', master.docker.port); while ((await replica.client.clusterSlots()).length === 0) { @@ -151,7 +160,8 @@ async function spawnRedisClusterNodeDockers( async function spawnRedisClusterNodeDocker( dockersConfig: RedisClusterDockersConfig, - serverArguments: Array + serverArguments: Array, + clientConfig?: Partial ) { const docker = await spawnRedisServerDocker(dockersConfig, [ ...serverArguments, @@ -163,7 +173,8 @@ async function spawnRedisClusterNodeDocker( client = createClient({ socket: { port: docker.port - } + }, + ...clientConfig }); await client.connect(); @@ -178,7 +189,8 @@ const SLOTS = 16384; async function spawnRedisClusterDockers( dockersConfig: RedisClusterDockersConfig, - serverArguments: Array + serverArguments: Array, + clientConfig?: Partial ): Promise> { const numberOfMasters = dockersConfig.numberOfMasters ?? 2, slotsPerNode = Math.floor(SLOTS / numberOfMasters), @@ -191,7 +203,8 @@ async function spawnRedisClusterDockers( dockersConfig, serverArguments, fromSlot, - toSlot + toSlot, + clientConfig ) ); } @@ -234,13 +247,18 @@ function totalNodes(slots: any) { const RUNNING_CLUSTERS = new Map, ReturnType>(); -export function spawnRedisCluster(dockersConfig: RedisClusterDockersConfig, serverArguments: Array): Promise> { +export function spawnRedisCluster( + dockersConfig: RedisClusterDockersConfig, + serverArguments: Array, + clientConfig?: Partial): Promise> { + const runningCluster = RUNNING_CLUSTERS.get(serverArguments); if (runningCluster) { return runningCluster; } - const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments); + const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments,clientConfig); + RUNNING_CLUSTERS.set(serverArguments, dockersPromise); return dockersPromise; } diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 87ba34db7ef..9dee350e31e 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -290,7 +290,8 @@ export default class TestUtils { ...dockerImage, numberOfMasters: options.numberOfMasters, numberOfReplicas: options.numberOfReplicas - }, options.serverArguments); + }, options.serverArguments, + options.clusterConfiguration?.defaults); return dockersPromise; }); } diff --git a/tsconfig.json b/tsconfig.json index a578fefa54f..8f43ab41d22 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,20 +1,32 @@ { "files": [], - "references": [{ - "path": "./packages/client" - }, { - "path": "./packages/test-utils" - }, { - "path": "./packages/bloom" - }, { - "path": "./packages/graph" - }, { - "path": "./packages/json" - }, { - "path": "./packages/search" - }, { - "path": "./packages/time-series" - }, { - "path": "./packages/redis" - }] + "references": [ + { + "path": "./packages/client" + }, + { + "path": "./packages/test-utils" + }, + { + "path": "./packages/bloom" + }, + { + "path": "./packages/graph" + }, + { + "path": "./packages/json" + }, + { + "path": "./packages/search" + }, + { + "path": "./packages/time-series" + }, + { + "path": "./packages/entraid" + }, + { + "path": "./packages/redis" + } + ] } From 6066692cb936039ae0dfc19c28ee22a1e2ebfbed Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:02:54 +0200 Subject: [PATCH 024/105] Release client@5.0.0-next.6 --- packages/client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/package.json b/packages/client/package.json index 9d028aa2bb2..cc892a12a49 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From c9215d18bf857f9ebaa0b2dc3d52d5591b4e52ea Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:07:40 +0200 Subject: [PATCH 025/105] Updated the EntraID package to use client@5.0.0-next.6 --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 571d78c0417..8041e9bbe29 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -19,7 +19,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" }, "devDependencies": { "@types/express": "^4.17.21", From 998b7f0ebd04496963ca844344b0a7f3320dc3d4 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:09:31 +0200 Subject: [PATCH 026/105] Release entraid@5.0.0-next.6 --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 8041e9bbe29..da0d7df9935 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 68e8973fd02cd55d6c537415fbe401dc49783ad9 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:11:21 +0200 Subject: [PATCH 027/105] Updated the JSON package to use client@5.0.0-next.6 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index 6cc43916497..b59f65ebc80 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" }, "devDependencies": { "@redis/test-utils": "*" From c0da3db8cfcc0cfb847974e1c3e22244fee54431 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:11:58 +0200 Subject: [PATCH 028/105] Release json@5.0.0-next.6 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index b59f65ebc80..b7a972c8c7a 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 153346b9aa3497e07be3848b1462c418ad6e8108 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:12:56 +0200 Subject: [PATCH 029/105] Updated the search package to use client@5.0.0-next.6 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index c5b75110f0f..f139c4ea8ba 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" }, "devDependencies": { "@redis/test-utils": "*" From f30926c91a3b08dc9f5a11fc08f2437794f0ba4f Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:13:34 +0200 Subject: [PATCH 030/105] Release search@5.0.0-next.6 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index f139c4ea8ba..26cbbaf5ad9 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 81a40a7020e7ba5829b32d8f122f03f19315fe59 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:14:30 +0200 Subject: [PATCH 031/105] Updated the timeseries package to use client@5.0.0-next.6 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 1e41ee237c3..819b6fdb6cd 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" }, "devDependencies": { "@redis/test-utils": "*" From a424058d374bf77ccc1807a246e2f40c77fac00b Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:15:11 +0200 Subject: [PATCH 032/105] Release time-series@5.0.0-next.6 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 819b6fdb6cd..22cfe8cefba 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From aecd625901a8691401d07d548629e574f958f9a9 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:16:57 +0200 Subject: [PATCH 033/105] Updated the bloom package to use client@5.0.0-next.6 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index cbcbc8ce2bd..a571451b8e3 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" }, "devDependencies": { "@redis/test-utils": "*" From 0a748590a0abd0888da8a88064d85773586fbae1 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:17:29 +0200 Subject: [PATCH 034/105] Release bloom@5.0.0-next.6 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index a571451b8e3..434ba809f7c 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 81f5fc43b8ebdb79cfa49dd717bc00cd1f5d91ef Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:18:54 +0200 Subject: [PATCH 035/105] Updated the graph package to use client@5.0.0-next.6 --- packages/graph/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graph/package.json b/packages/graph/package.json index c62abb23892..1604826b054 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -12,7 +12,7 @@ "test-disable": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" }, "devDependencies": { "@redis/test-utils": "*" From 0f7e0f45ae4ae0fdc2477321e46bd9398ac28308 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:19:25 +0200 Subject: [PATCH 036/105] Release graph@5.0.0-next.6 --- packages/graph/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/graph/package.json b/packages/graph/package.json index 1604826b054..1ea08401f1b 100644 --- a/packages/graph/package.json +++ b/packages/graph/package.json @@ -1,6 +1,6 @@ { "name": "@redis/graph", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 70f3698134daf71e431b1fc2a4c25c692c11f1e7 Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:21:24 +0200 Subject: [PATCH 037/105] Updated the Redis package to use client@5.0.0-next.6 --- packages/redis/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/redis/package.json b/packages/redis/package.json index 617a5bff357..e99a229b11c 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -10,12 +10,12 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { - "@redis/bloom": "5.0.0-next.5", - "@redis/client": "5.0.0-next.5", - "@redis/graph": "5.0.0-next.5", - "@redis/json": "5.0.0-next.5", - "@redis/search": "5.0.0-next.5", - "@redis/time-series": "5.0.0-next.5" + "@redis/bloom": "5.0.0-next.6", + "@redis/client": "5.0.0-next.6", + "@redis/graph": "5.0.0-next.6", + "@redis/json": "5.0.0-next.6", + "@redis/search": "5.0.0-next.6", + "@redis/time-series": "5.0.0-next.6" }, "engines": { "node": ">= 18" From aa4ea3317f6b6eb77e9f7733931a9a3cab39b3bd Mon Sep 17 00:00:00 2001 From: dmaier-redislabs Date: Thu, 30 Jan 2025 11:26:52 +0200 Subject: [PATCH 038/105] Release redis@5.0.0-next.6 --- packages/redis/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/redis/package.json b/packages/redis/package.json index e99a229b11c..5069d936ba8 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 558ebb4d25f2cbeb707cb601d4bd11014a6a6992 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Thu, 30 Jan 2025 12:46:41 +0200 Subject: [PATCH 039/105] Update README.md (#2890) --- packages/entraid/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/README.md b/packages/entraid/README.md index e9c7956022e..131935fe619 100644 --- a/packages/entraid/README.md +++ b/packages/entraid/README.md @@ -36,7 +36,7 @@ The first step to using @redis/entraid is choosing the right credentials provide ```typescript import { createClient } from '@redis/client'; -import { EntraIdCredentialsProviderFactory } from '@redis/entraid'; +import { EntraIdCredentialsProviderFactory } from '@redis/entraid/dist/lib/entra-id-credentials-provider-factory'; const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({ clientId: 'your-client-id', From 1af01373dbeac4bca700158a13a22627f6ef05f2 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Mon, 17 Feb 2025 13:47:12 +0200 Subject: [PATCH 040/105] feat(search): Set default dialect to 2 for Redis Search commands (#2895) - The default dialect `DEFAULT_DIALECT` is now set to '2' - Automatically append DIALECT parameter to search commands when not explicitly specified --- .../search/lib/commands/AGGREGATE.spec.ts | 69 ++++++++++--------- packages/search/lib/commands/AGGREGATE.ts | 9 ++- .../lib/commands/AGGREGATE_WITHCURSOR.spec.ts | 7 +- packages/search/lib/commands/EXPLAIN.spec.ts | 5 +- packages/search/lib/commands/EXPLAIN.ts | 3 + .../search/lib/commands/EXPLAINCLI.spec.ts | 10 ++- packages/search/lib/commands/EXPLAINCLI.ts | 18 ++++- .../lib/commands/PROFILE_AGGREGATE.spec.ts | 5 +- .../lib/commands/PROFILE_SEARCH.spec.ts | 6 +- packages/search/lib/commands/SEARCH.spec.ts | 52 +++++++------- packages/search/lib/commands/SEARCH.ts | 5 +- .../lib/commands/SEARCH_NOCONTENT.spec.ts | 3 +- .../search/lib/commands/SEARCH_NOCONTENT.ts | 2 +- .../search/lib/commands/SPELLCHECK.spec.ts | 9 +-- packages/search/lib/commands/SPELLCHECK.ts | 3 + packages/search/lib/dialect/default.ts | 1 + 16 files changed, 126 insertions(+), 81 deletions(-) create mode 100644 packages/search/lib/dialect/default.ts diff --git a/packages/search/lib/commands/AGGREGATE.spec.ts b/packages/search/lib/commands/AGGREGATE.spec.ts index 787fbd1472f..420911c5600 100644 --- a/packages/search/lib/commands/AGGREGATE.spec.ts +++ b/packages/search/lib/commands/AGGREGATE.spec.ts @@ -2,13 +2,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import AGGREGATE from './AGGREGATE'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; -describe('AGGREGATE', () => { +describe('AGGREGATE', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( parseArgs(AGGREGATE, 'index', '*'), - ['FT.AGGREGATE', 'index', '*'] + ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -17,14 +18,14 @@ describe('AGGREGATE', () => { parseArgs(AGGREGATE, 'index', '*', { VERBATIM: true }), - ['FT.AGGREGATE', 'index', '*', 'VERBATIM'] + ['FT.AGGREGATE', 'index', '*', 'VERBATIM', 'DIALECT', DEFAULT_DIALECT] ); }); it('with ADDSCORES', () => { assert.deepEqual( parseArgs(AGGREGATE, 'index', '*', { ADDSCORES: true }), - ['FT.AGGREGATE', 'index', '*', 'ADDSCORES'] + ['FT.AGGREGATE', 'index', '*', 'ADDSCORES', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -36,7 +37,7 @@ describe('AGGREGATE', () => { parseArgs(AGGREGATE, 'index', '*', { LOAD: '@property' }), - ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -47,7 +48,7 @@ describe('AGGREGATE', () => { identifier: '@property' } }), - ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'LOAD', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -60,7 +61,7 @@ describe('AGGREGATE', () => { AS: 'alias' } }), - ['FT.AGGREGATE', 'index', '*', 'LOAD', '3', '@property', 'AS', 'alias'] + ['FT.AGGREGATE', 'index', '*', 'LOAD', '3', '@property', 'AS', 'alias', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -70,7 +71,7 @@ describe('AGGREGATE', () => { parseArgs(AGGREGATE, 'index', '*', { LOAD: ['@1', '@2'] }), - ['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2'] + ['FT.AGGREGATE', 'index', '*', 'LOAD', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -89,7 +90,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -104,7 +105,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'AS', 'count'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT', '0', 'AS', 'count', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -121,7 +122,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '1', '@property', 'REDUCE', 'COUNT', '0'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '1', '@property', 'REDUCE', 'COUNT', '0', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -136,7 +137,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '2', '@1', '@2', 'REDUCE', 'COUNT', '0'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '2', '@1', '@2', 'REDUCE', 'COUNT', '0', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -153,7 +154,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCT', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCT', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -168,7 +169,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -183,7 +184,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'SUM', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'SUM', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -198,7 +199,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MIN', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MIN', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -213,7 +214,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MAX', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'MAX', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -228,7 +229,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'AVG', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'AVG', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); it('STDDEV', () => { @@ -242,7 +243,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'STDDEV', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'STDDEV', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -258,7 +259,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'QUANTILE', '2', '@property', '0.5'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'QUANTILE', '2', '@property', '0.5', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -273,7 +274,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'TOLIST', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'TOLIST', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -289,7 +290,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '1', '@property'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '1', '@property', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -307,7 +308,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -326,7 +327,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '3', '@property', 'BY', '@by', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -346,7 +347,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '4', '@property', 'BY', '@by', 'ASC'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'FIRST_VALUE', '4', '@property', 'BY', '@by', 'ASC', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -364,7 +365,7 @@ describe('AGGREGATE', () => { } }] }), - ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'RANDOM_SAMPLE', '2', '@property', '1'] + ['FT.AGGREGATE', 'index', '*', 'GROUPBY', '0', 'REDUCE', 'RANDOM_SAMPLE', '2', '@property', '1', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -378,7 +379,7 @@ describe('AGGREGATE', () => { BY: '@by' }] }), - ['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by'] + ['FT.AGGREGATE', 'index', '*', 'SORTBY', '1', '@by', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -390,7 +391,7 @@ describe('AGGREGATE', () => { BY: ['@1', '@2'] }] }), - ['FT.AGGREGATE', 'index', '*', 'SORTBY', '2', '@1', '@2'] + ['FT.AGGREGATE', 'index', '*', 'SORTBY', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -403,7 +404,7 @@ describe('AGGREGATE', () => { MAX: 1 }] }), - ['FT.AGGREGATE', 'index', '*', 'SORTBY', '3', '@by', 'MAX', '1'] + ['FT.AGGREGATE', 'index', '*', 'SORTBY', '3', '@by', 'MAX', '1', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -417,7 +418,7 @@ describe('AGGREGATE', () => { AS: 'as' }] }), - ['FT.AGGREGATE', 'index', '*', 'APPLY', '@field + 1', 'AS', 'as'] + ['FT.AGGREGATE', 'index', '*', 'APPLY', '@field + 1', 'AS', 'as', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -430,7 +431,7 @@ describe('AGGREGATE', () => { size: 1 }] }), - ['FT.AGGREGATE', 'index', '*', 'LIMIT', '0', '1'] + ['FT.AGGREGATE', 'index', '*', 'LIMIT', '0', '1', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -442,7 +443,7 @@ describe('AGGREGATE', () => { expression: '@field != ""' }] }), - ['FT.AGGREGATE', 'index', '*', 'FILTER', '@field != ""'] + ['FT.AGGREGATE', 'index', '*', 'FILTER', '@field != ""', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -454,7 +455,7 @@ describe('AGGREGATE', () => { param: 'value' } }), - ['FT.AGGREGATE', 'index', '*', 'PARAMS', '2', 'param', 'value'] + ['FT.AGGREGATE', 'index', '*', 'PARAMS', '2', 'param', 'value', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -470,7 +471,7 @@ describe('AGGREGATE', () => { it('with TIMEOUT', () => { assert.deepEqual( parseArgs(AGGREGATE, 'index', '*', { TIMEOUT: 10 }), - ['FT.AGGREGATE', 'index', '*', 'TIMEOUT', '10'] + ['FT.AGGREGATE', 'index', '*', 'TIMEOUT', '10', 'DIALECT', DEFAULT_DIALECT] ); }); }); diff --git a/packages/search/lib/commands/AGGREGATE.ts b/packages/search/lib/commands/AGGREGATE.ts index 925a91da008..b2589a52a54 100644 --- a/packages/search/lib/commands/AGGREGATE.ts +++ b/packages/search/lib/commands/AGGREGATE.ts @@ -3,6 +3,7 @@ import { ArrayReply, BlobStringReply, Command, MapReply, NumberReply, RedisArgum import { RediSearchProperty } from './CREATE'; import { FtSearchParams, parseParamsArgument } from './SEARCH'; import { transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; type LoadField = RediSearchProperty | { identifier: RediSearchProperty; @@ -12,12 +13,12 @@ type LoadField = RediSearchProperty | { export const FT_AGGREGATE_STEPS = { GROUPBY: 'GROUPBY', SORTBY: 'SORTBY', - APPLY: 'APPLY', + APPLY: 'APPLY', LIMIT: 'LIMIT', FILTER: 'FILTER' } as const; -type FT_AGGREGATE_STEPS = typeof FT_AGGREGATE_STEPS; +type FT_AGGREGATE_STEPS = typeof FT_AGGREGATE_STEPS; export type FtAggregateStep = FT_AGGREGATE_STEPS[keyof FT_AGGREGATE_STEPS]; @@ -249,8 +250,10 @@ export function parseAggregateOptions(parser: CommandParser , options?: FtAggreg parseParamsArgument(parser, options?.PARAMS); - if (options?.DIALECT !== undefined) { + if (options?.DIALECT) { parser.push('DIALECT', options.DIALECT.toString()); + } else { + parser.push('DIALECT', DEFAULT_DIALECT); } } diff --git a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts index 57f46d1e32c..0e89346c49f 100644 --- a/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts +++ b/packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts @@ -2,13 +2,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; describe('AGGREGATE WITHCURSOR', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( parseArgs(AGGREGATE_WITHCURSOR, 'index', '*'), - ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR'] + ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT, 'WITHCURSOR'] ); }); @@ -17,7 +18,7 @@ describe('AGGREGATE WITHCURSOR', () => { parseArgs(AGGREGATE_WITHCURSOR, 'index', '*', { COUNT: 1 }), - ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'COUNT', '1'] + ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT, 'WITHCURSOR', 'COUNT', '1'] ); }); @@ -26,7 +27,7 @@ describe('AGGREGATE WITHCURSOR', () => { parseArgs(AGGREGATE_WITHCURSOR, 'index', '*', { MAXIDLE: 1 }), - ['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'MAXIDLE', '1'] + ['FT.AGGREGATE', 'index', '*', 'DIALECT', DEFAULT_DIALECT, 'WITHCURSOR', 'MAXIDLE', '1'] ); }); }); diff --git a/packages/search/lib/commands/EXPLAIN.spec.ts b/packages/search/lib/commands/EXPLAIN.spec.ts index ddc551fbd71..d1691bc7c25 100644 --- a/packages/search/lib/commands/EXPLAIN.spec.ts +++ b/packages/search/lib/commands/EXPLAIN.spec.ts @@ -3,13 +3,14 @@ import EXPLAIN from './EXPLAIN'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; import testUtils, { GLOBAL } from '../test-utils'; import { SCHEMA_FIELD_TYPE } from './CREATE'; +import { DEFAULT_DIALECT } from '../dialect/default'; describe('EXPLAIN', () => { describe('transformArguments', () => { it('simple', () => { assert.deepEqual( parseArgs(EXPLAIN, 'index', '*'), - ['FT.EXPLAIN', 'index', '*'] + ['FT.EXPLAIN', 'index', '*', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -20,7 +21,7 @@ describe('EXPLAIN', () => { param: 'value' } }), - ['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value'] + ['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value', 'DIALECT', DEFAULT_DIALECT] ); }); diff --git a/packages/search/lib/commands/EXPLAIN.ts b/packages/search/lib/commands/EXPLAIN.ts index dcd7c3c0d2d..39a430f4371 100644 --- a/packages/search/lib/commands/EXPLAIN.ts +++ b/packages/search/lib/commands/EXPLAIN.ts @@ -1,6 +1,7 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; import { FtSearchParams, parseParamsArgument } from './SEARCH'; +import { DEFAULT_DIALECT } from '../dialect/default'; export interface FtExplainOptions { PARAMS?: FtSearchParams; @@ -22,6 +23,8 @@ export default { if (options?.DIALECT) { parser.push('DIALECT', options.DIALECT.toString()); + } else { + parser.push('DIALECT', DEFAULT_DIALECT); } }, transformReply: undefined as unknown as () => SimpleStringReply diff --git a/packages/search/lib/commands/EXPLAINCLI.spec.ts b/packages/search/lib/commands/EXPLAINCLI.spec.ts index cf46a1740c5..1812b674094 100644 --- a/packages/search/lib/commands/EXPLAINCLI.spec.ts +++ b/packages/search/lib/commands/EXPLAINCLI.spec.ts @@ -1,12 +1,20 @@ import { strict as assert } from 'node:assert'; import EXPLAINCLI from './EXPLAINCLI'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; describe('EXPLAINCLI', () => { it('transformArguments', () => { assert.deepEqual( parseArgs(EXPLAINCLI, 'index', '*'), - ['FT.EXPLAINCLI', 'index', '*'] + ['FT.EXPLAINCLI', 'index', '*', 'DIALECT', DEFAULT_DIALECT] + ); + }); + + it('with dialect', () => { + assert.deepEqual( + parseArgs(EXPLAINCLI, 'index', '*', {DIALECT: 1}), + ['FT.EXPLAINCLI', 'index', '*', 'DIALECT', '1'] ); }); }); diff --git a/packages/search/lib/commands/EXPLAINCLI.ts b/packages/search/lib/commands/EXPLAINCLI.ts index 53711067058..4ef5fba88d6 100644 --- a/packages/search/lib/commands/EXPLAINCLI.ts +++ b/packages/search/lib/commands/EXPLAINCLI.ts @@ -1,11 +1,27 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; +import { DEFAULT_DIALECT } from '../dialect/default'; + +export interface FtExplainCLIOptions { + DIALECT?: number; +} export default { NOT_KEYED_COMMAND: true, IS_READ_ONLY: true, - parseCommand(parser: CommandParser, index: RedisArgument, query: RedisArgument) { + parseCommand( + parser: CommandParser, + index: RedisArgument, + query: RedisArgument, + options?: FtExplainCLIOptions + ) { parser.push('FT.EXPLAINCLI', index, query); + + if (options?.DIALECT) { + parser.push('DIALECT', options.DIALECT.toString()); + } else { + parser.push('DIALECT', DEFAULT_DIALECT); + } }, transformReply: undefined as unknown as () => ArrayReply } as const satisfies Command; diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts index ee112118c95..5cfa0500a0a 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts @@ -4,13 +4,14 @@ import { FT_AGGREGATE_STEPS } from './AGGREGATE'; import PROFILE_AGGREGATE from './PROFILE_AGGREGATE'; import { SCHEMA_FIELD_TYPE } from './CREATE'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; describe('PROFILE AGGREGATE', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( parseArgs(PROFILE_AGGREGATE, 'index', 'query'), - ['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query'] + ['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -25,7 +26,7 @@ describe('PROFILE AGGREGATE', () => { }] }), ['FT.PROFILE', 'index', 'AGGREGATE', 'LIMITED', 'QUERY', 'query', - 'VERBATIM', 'SORTBY', '1', '@by'] + 'VERBATIM', 'SORTBY', '1', '@by', 'DIALECT', DEFAULT_DIALECT] ); }); }); diff --git a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts index 524ff1a5228..60f1e8b7474 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts @@ -3,14 +3,14 @@ import testUtils, { GLOBAL } from '../test-utils'; import PROFILE_SEARCH from './PROFILE_SEARCH'; import { SCHEMA_FIELD_TYPE } from './CREATE'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - +import { DEFAULT_DIALECT } from '../dialect/default'; describe('PROFILE SEARCH', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( parseArgs(PROFILE_SEARCH, 'index', 'query'), - ['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query'] + ['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -22,7 +22,7 @@ describe('PROFILE SEARCH', () => { INKEYS: 'key' }), ['FT.PROFILE', 'index', 'SEARCH', 'LIMITED', 'QUERY', 'query', - 'VERBATIM', 'INKEYS', '1', 'key'] + 'VERBATIM', 'INKEYS', '1', 'key', 'DIALECT', DEFAULT_DIALECT] ); }); }); diff --git a/packages/search/lib/commands/SEARCH.spec.ts b/packages/search/lib/commands/SEARCH.spec.ts index 24248b4cf15..ab480808ffa 100644 --- a/packages/search/lib/commands/SEARCH.spec.ts +++ b/packages/search/lib/commands/SEARCH.spec.ts @@ -2,13 +2,15 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SEARCH from './SEARCH'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; + describe('FT.SEARCH', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( parseArgs(SEARCH, 'index', 'query'), - ['FT.SEARCH', 'index', 'query'] + ['FT.SEARCH', 'index', 'query', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -17,7 +19,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { VERBATIM: true }), - ['FT.SEARCH', 'index', 'query', 'VERBATIM'] + ['FT.SEARCH', 'index', 'query', 'VERBATIM', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -26,7 +28,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { NOSTOPWORDS: true }), - ['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS'] + ['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -35,7 +37,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { INKEYS: 'key' }), - ['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key'] + ['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -44,7 +46,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { INFIELDS: 'field' }), - ['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field'] + ['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -53,7 +55,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { RETURN: 'return' }), - ['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return'] + ['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -63,7 +65,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { SUMMARIZE: true }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE'] + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -75,7 +77,7 @@ describe('FT.SEARCH', () => { FIELDS: '@field' } }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field'] + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -86,7 +88,7 @@ describe('FT.SEARCH', () => { FIELDS: ['@1', '@2'] } }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2'] + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -98,7 +100,7 @@ describe('FT.SEARCH', () => { FRAGS: 1 } }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1'] + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -109,7 +111,7 @@ describe('FT.SEARCH', () => { LEN: 1 } }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1'] + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -120,7 +122,7 @@ describe('FT.SEARCH', () => { SEPARATOR: 'separator' } }), - ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator'] + ['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -131,7 +133,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { HIGHLIGHT: true }), - ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT'] + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -143,7 +145,7 @@ describe('FT.SEARCH', () => { FIELDS: ['@field'] } }), - ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field'] + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -154,7 +156,7 @@ describe('FT.SEARCH', () => { FIELDS: ['@1', '@2'] } }), - ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2'] + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -169,7 +171,7 @@ describe('FT.SEARCH', () => { } } }), - ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close'] + ['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close', 'DIALECT', DEFAULT_DIALECT] ); }); }); @@ -179,7 +181,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { SLOP: 1 }), - ['FT.SEARCH', 'index', 'query', 'SLOP', '1'] + ['FT.SEARCH', 'index', 'query', 'SLOP', '1', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -188,7 +190,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { TIMEOUT: 1 }), - ['FT.SEARCH', 'index', 'query', 'TIMEOUT', '1'] + ['FT.SEARCH', 'index', 'query', 'TIMEOUT', '1', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -197,7 +199,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { INORDER: true }), - ['FT.SEARCH', 'index', 'query', 'INORDER'] + ['FT.SEARCH', 'index', 'query', 'INORDER', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -206,7 +208,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { LANGUAGE: 'Arabic' }), - ['FT.SEARCH', 'index', 'query', 'LANGUAGE', 'Arabic'] + ['FT.SEARCH', 'index', 'query', 'LANGUAGE', 'Arabic', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -215,7 +217,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { EXPANDER: 'expender' }), - ['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender'] + ['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -224,7 +226,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { SCORER: 'scorer' }), - ['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer'] + ['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -233,7 +235,7 @@ describe('FT.SEARCH', () => { parseArgs(SEARCH, 'index', 'query', { SORTBY: '@by' }), - ['FT.SEARCH', 'index', 'query', 'SORTBY', '@by'] + ['FT.SEARCH', 'index', 'query', 'SORTBY', '@by', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -245,7 +247,7 @@ describe('FT.SEARCH', () => { size: 1 } }), - ['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1'] + ['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -258,7 +260,7 @@ describe('FT.SEARCH', () => { number: 1 } }), - ['FT.SEARCH', 'index', 'query', 'PARAMS', '6', 'string', 'string', 'buffer', Buffer.from('buffer'), 'number', '1'] + ['FT.SEARCH', 'index', 'query', 'PARAMS', '6', 'string', 'string', 'buffer', Buffer.from('buffer'), 'number', '1', 'DIALECT', DEFAULT_DIALECT] ); }); diff --git a/packages/search/lib/commands/SEARCH.ts b/packages/search/lib/commands/SEARCH.ts index c2d2a9d2696..f48ac056784 100644 --- a/packages/search/lib/commands/SEARCH.ts +++ b/packages/search/lib/commands/SEARCH.ts @@ -2,6 +2,7 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers'; import { RediSearchProperty, RediSearchLanguage } from './CREATE'; +import { DEFAULT_DIALECT } from '../dialect/default'; export type FtSearchParams = Record; @@ -150,8 +151,10 @@ export function parseSearchOptions(parser: CommandParser, options?: FtSearchOpti parseParamsArgument(parser, options?.PARAMS); - if (options?.DIALECT !== undefined) { + if (options?.DIALECT) { parser.push('DIALECT', options.DIALECT.toString()); + } else { + parser.push('DIALECT', DEFAULT_DIALECT); } } diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts index bfcca8b4bda..cd37409b5bb 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts @@ -2,13 +2,14 @@ import { strict as assert } from 'assert'; import testUtils, { GLOBAL } from '../test-utils'; import SEARCH_NOCONTENT from './SEARCH_NOCONTENT'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; describe('FT.SEARCH NOCONTENT', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( parseArgs(SEARCH_NOCONTENT, 'index', 'query'), - ['FT.SEARCH', 'index', 'query', 'NOCONTENT'] + ['FT.SEARCH', 'index', 'query', 'DIALECT', DEFAULT_DIALECT, 'NOCONTENT'] ); }); }); diff --git a/packages/search/lib/commands/SEARCH_NOCONTENT.ts b/packages/search/lib/commands/SEARCH_NOCONTENT.ts index c0a152375d9..a6968851acd 100644 --- a/packages/search/lib/commands/SEARCH_NOCONTENT.ts +++ b/packages/search/lib/commands/SEARCH_NOCONTENT.ts @@ -5,7 +5,7 @@ export default { NOT_KEYED_COMMAND: SEARCH.NOT_KEYED_COMMAND, IS_READ_ONLY: SEARCH.IS_READ_ONLY, parseCommand(...args: Parameters) { - SEARCH.parseCommand(...args); + SEARCH.parseCommand(...args); args[0].push('NOCONTENT'); }, transformReply: { diff --git a/packages/search/lib/commands/SPELLCHECK.spec.ts b/packages/search/lib/commands/SPELLCHECK.spec.ts index 4f5a3628f4d..482deed6a45 100644 --- a/packages/search/lib/commands/SPELLCHECK.spec.ts +++ b/packages/search/lib/commands/SPELLCHECK.spec.ts @@ -2,13 +2,14 @@ import { strict as assert } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import SPELLCHECK from './SPELLCHECK'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; +import { DEFAULT_DIALECT } from '../dialect/default'; describe('FT.SPELLCHECK', () => { describe('transformArguments', () => { it('without options', () => { assert.deepEqual( parseArgs(SPELLCHECK, 'index', 'query'), - ['FT.SPELLCHECK', 'index', 'query'] + ['FT.SPELLCHECK', 'index', 'query', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -17,7 +18,7 @@ describe('FT.SPELLCHECK', () => { parseArgs(SPELLCHECK, 'index', 'query', { DISTANCE: 2 }), - ['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2'] + ['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -30,7 +31,7 @@ describe('FT.SPELLCHECK', () => { dictionary: 'dictionary' } }), - ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary'] + ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary', 'DIALECT', DEFAULT_DIALECT] ); }); @@ -45,7 +46,7 @@ describe('FT.SPELLCHECK', () => { dictionary: 'exclude' }] }), - ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude'] + ['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude', 'DIALECT', DEFAULT_DIALECT] ); }); }); diff --git a/packages/search/lib/commands/SPELLCHECK.ts b/packages/search/lib/commands/SPELLCHECK.ts index ae95b72c249..3b909cdca32 100644 --- a/packages/search/lib/commands/SPELLCHECK.ts +++ b/packages/search/lib/commands/SPELLCHECK.ts @@ -1,5 +1,6 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; import { RedisArgument, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types'; +import { DEFAULT_DIALECT } from '../dialect/default'; export interface Terms { mode: 'INCLUDE' | 'EXCLUDE'; @@ -34,6 +35,8 @@ export default { if (options?.DIALECT) { parser.push('DIALECT', options.DIALECT.toString()); + } else { + parser.push('DIALECT', DEFAULT_DIALECT); } }, transformReply: { diff --git a/packages/search/lib/dialect/default.ts b/packages/search/lib/dialect/default.ts new file mode 100644 index 00000000000..54cde05d119 --- /dev/null +++ b/packages/search/lib/dialect/default.ts @@ -0,0 +1 @@ +export const DEFAULT_DIALECT = '2'; From 33cdc007466b336f7f7c82669e8312d06df83a31 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Tue, 18 Feb 2025 15:57:40 +0200 Subject: [PATCH 041/105] churn(docs) update entraid documentation (#2898) --- README.md | 1 + packages/entraid/README.md | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 990204eb3df..457f3d2c2cf 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ npm install redis | [`@redis/json`](./packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | | [`@redis/search`](./packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | | [`@redis/time-series`](./packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | +| [`@redis/entraid`](./packages/entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | > Looking for a high-level library to handle object mapping? See [redis-om-node](https://github.com/redis/redis-om-node)! diff --git a/packages/entraid/README.md b/packages/entraid/README.md index 131935fe619..eec88d71360 100644 --- a/packages/entraid/README.md +++ b/packages/entraid/README.md @@ -15,9 +15,10 @@ Secure token-based authentication for Redis clients using Microsoft Entra ID (fo ## Installation + ```bash -npm install @redis/client -npm install @redis/entraid +npm install "@redis/client@5.0.0-next.6" +npm install "@redis/entraid@5.0.0-next.6" ``` ## Getting Started From 69d507a572a984e233090467fd960a7d6c47bb64 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Thu, 27 Feb 2025 10:56:58 +0200 Subject: [PATCH 042/105] refactor!: redis 8 compatibility improvements and test infrastructure updates (#2893) * churn(test): use redislabs/client-libs-test for testing This switches our testing infrastructure from redis/redis-stack to redislabs/client-libs-test Docker image across all packages. This change also updates the default Docker version from 7.4.0-v1 to 8.0-M04-pre. * churn(test): verify CONFIG SET / GET compatibility with Redis 8 - Add tests for Redis 8 search configuration settings - Deprecate Redis Search CONFIG commands in favor of standard CONFIG - Test read-only config restrictions for Redis 8 * churn(test): handle Redis 8 coordinate precision in GEOPOS - Update GEOPOS tests to handle increased precision in Redis 8 (17 decimal places vs 14) - Add precision-aware coordinate comparison helper - Add comprehensive test suite for coordinate comparison function * test(search): adapt SUGGET tests for Redis 8 empty results - Update tests to expect empty array ([]) instead of null for SUGGET variants - Affects sugGet, sugGetWithPayloads, sugGetWithScores, and sugGetWithScoresWithPayloads * test(search): support Redis 8 INFO indexes_all field - Add indexes_all field introduced in Redis 8 to index definition test * refactor!(search): simplify PROFILE commands to return raw response - BREAKING CHANGE: FT.PROFILE now returns raw response, letting users implement their own parsing * test: improve version-specific test coverage - Add `testWithClientIfVersionWithinRange` method to run tests for specific Redis versions - Refactor TestUtils to handle version comparisons more accurately - Update test utilities across Redis modules to run tests against multiple versions, and not against latest only --- .github/workflows/tests.yml | 4 +- package.json | 1 + packages/bloom/lib/test-utils.ts | 8 +- .../client/lib/commands/CONFIG_GET.spec.ts | 30 +- .../client/lib/commands/CONFIG_SET.spec.ts | 9 + packages/client/lib/commands/GEOPOS.spec.ts | 124 +++++- packages/client/lib/test-utils.ts | 6 +- packages/entraid/lib/test-utils.ts | 6 +- packages/graph/lib/test-utils.ts | 9 +- packages/json/lib/test-utils.ts | 8 +- .../search/lib/commands/CONFIG_SET.spec.ts | 36 ++ packages/search/lib/commands/INFO.spec.ts | 396 +++++++++++++----- .../lib/commands/PROFILE_AGGREGATE.spec.ts | 137 ++++-- .../search/lib/commands/PROFILE_AGGREGATE.ts | 60 ++- .../lib/commands/PROFILE_SEARCH.spec.ts | 117 ++++-- .../search/lib/commands/PROFILE_SEARCH.ts | 133 +----- packages/search/lib/commands/SUGGET.spec.ts | 16 +- .../lib/commands/SUGGET_WITHPAYLOADS.spec.ts | 21 +- .../lib/commands/SUGGET_WITHSCORES.spec.ts | 9 +- .../SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts | 8 +- packages/search/lib/commands/index.ts | 12 + packages/search/lib/test-utils.ts | 35 +- packages/search/package.json | 3 +- packages/test-utils/lib/dockers.ts | 51 ++- packages/test-utils/lib/index.spec.ts | 106 +++++ packages/test-utils/lib/index.ts | 149 +++++-- packages/test-utils/package.json | 3 + packages/time-series/lib/test-utils.ts | 8 +- 28 files changed, 1083 insertions(+), 422 deletions(-) create mode 100644 packages/test-utils/lib/index.spec.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9decd26898a..7bcc72e5408 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -21,8 +21,8 @@ jobs: strategy: fail-fast: false matrix: - node-version: ['18', '20', '22'] - redis-version: ['6.2.6-v17', '7.2.0-v13', '7.4.0-v1'] + node-version: [ '18', '20', '22' ] + redis-version: [ 'rs-7.2.0-v13', 'rs-7.4.0-v1', '8.0-M04-pre' ] steps: - uses: actions/checkout@v4 with: diff --git a/package.json b/package.json index 7ab2a557ff2..0a29c71f831 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "./packages/*" ], "scripts": { + "test-single": "TS_NODE_PROJECT='./packages/test-utils/tsconfig.json' mocha --require ts-node/register/transpile-only ", "test": "npm run test -ws --if-present", "build": "tsc --build", "documentation": "typedoc --out ./documentation", diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 1291054e802..7996d61e44e 100644 --- a/packages/bloom/lib/test-utils.ts +++ b/packages/bloom/lib/test-utils.ts @@ -1,10 +1,10 @@ import TestUtils from '@redis/test-utils'; import RedisBloomModules from '.'; -export default new TestUtils({ - dockerImageName: 'redis/redis-stack', - dockerImageVersionArgument: 'redisbloom-version', - defaultDockerVersion: '7.4.0-v1' +export default TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M04-pre' }); export const GLOBAL = { diff --git a/packages/client/lib/commands/CONFIG_GET.spec.ts b/packages/client/lib/commands/CONFIG_GET.spec.ts index 411b2ddf472..c3f0eac76dd 100644 --- a/packages/client/lib/commands/CONFIG_GET.spec.ts +++ b/packages/client/lib/commands/CONFIG_GET.spec.ts @@ -19,7 +19,6 @@ describe('CONFIG GET', () => { ); }); }); - testUtils.testWithClient('client.configGet', async client => { const config = await client.configGet('*'); @@ -29,4 +28,33 @@ describe('CONFIG GET', () => { assert.equal(typeof value, 'string'); } }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('client.configSet.getSearchConfigSettingTest | Redis >= 8', async client => { + assert.ok( + await client.configGet('search-timeout'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('client.configSet.getTSConfigSettingTest | Redis >= 8', async client => { + assert.ok( + await client.configGet('ts-retention-policy'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('client.configSet.getBFConfigSettingTest | Redis >= 8', async client => { + assert.ok( + await client.configGet('bf-error-rate'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('client.configSet.getCFConfigSettingTest | Redis >= 8', async client => { + assert.ok( + await client.configGet('cf-initial-size'), + 'OK' + ); + }, GLOBAL.SERVERS.OPEN); + }); diff --git a/packages/client/lib/commands/CONFIG_SET.spec.ts b/packages/client/lib/commands/CONFIG_SET.spec.ts index 56bed2ac46a..f9f34dec937 100644 --- a/packages/client/lib/commands/CONFIG_SET.spec.ts +++ b/packages/client/lib/commands/CONFIG_SET.spec.ts @@ -30,4 +30,13 @@ describe('CONFIG SET', () => { 'OK' ); }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClient('client.configSet.setReadOnlySearchConfigTest | Redis >= 8', + async client => { + assert.rejects( + client.configSet('search-max-doctablesize', '0'), + new Error('ERR CONFIG SET failed (possibly related to argument \'search-max-doctablesize\') - can\'t set immutable config') + ); + }, GLOBAL.SERVERS.OPEN); + }); diff --git a/packages/client/lib/commands/GEOPOS.spec.ts b/packages/client/lib/commands/GEOPOS.spec.ts index 247dd91d222..002d16d0256 100644 --- a/packages/client/lib/commands/GEOPOS.spec.ts +++ b/packages/client/lib/commands/GEOPOS.spec.ts @@ -1,4 +1,4 @@ -import { strict as assert } from 'node:assert'; +import { strict as assert, fail } from 'node:assert'; import testUtils, { GLOBAL } from '../test-utils'; import GEOPOS from './GEOPOS'; import { parseArgs } from './generic-transformers'; @@ -41,12 +41,126 @@ describe('GEOPOS', () => { ...coordinates }); - assert.deepEqual( - await client.geoPos('key', 'member'), - [coordinates] - ); + const result = await client.geoPos('key', 'member'); + + /** + * - Redis < 8: Returns coordinates with 14 decimal places (e.g., "-122.06429868936539") + * - Redis 8+: Returns coordinates with 17 decimal places (e.g., "-122.06429868936538696") + * + */ + const PRECISION = 13; // Number of decimal places to compare + + if (result && result.length === 1 && result[0] != null) { + const { longitude, latitude } = result[0]; + + assert.ok( + compareWithPrecision(longitude, coordinates.longitude, PRECISION), + `Longitude mismatch: ${longitude} vs ${coordinates.longitude}` + ); + assert.ok( + compareWithPrecision(latitude, coordinates.latitude, PRECISION), + `Latitude mismatch: ${latitude} vs ${coordinates.latitude}` + ); + + } else { + assert.fail('Expected a valid result'); + } + + + }, { client: GLOBAL.SERVERS.OPEN, cluster: GLOBAL.CLUSTERS.OPEN }); }); + +describe('compareWithPrecision', () => { + it('should match exact same numbers', () => { + assert.strictEqual( + compareWithPrecision('123.456789', '123.456789', 6), + true + ); + }); + + it('should match when actual has more precision than needed', () => { + assert.strictEqual( + compareWithPrecision('123.456789123456', '123.456789', 6), + true + ); + }); + + it('should match when expected has more precision than needed', () => { + assert.strictEqual( + compareWithPrecision('123.456789', '123.456789123456', 6), + true + ); + }); + + it('should fail when decimals differ within precision', () => { + assert.strictEqual( + compareWithPrecision('123.456689', '123.456789', 6), + false + ); + }); + + it('should handle negative numbers', () => { + assert.strictEqual( + compareWithPrecision('-122.06429868936538', '-122.06429868936539', 13), + true + ); + }); + + it('should fail when integer parts differ', () => { + assert.strictEqual( + compareWithPrecision('124.456789', '123.456789', 6), + false + ); + }); + + it('should handle zero decimal places', () => { + assert.strictEqual( + compareWithPrecision('123.456789', '123.456789', 0), + true + ); + }); + + it('should handle numbers without decimal points', () => { + assert.strictEqual( + compareWithPrecision('123', '123', 6), + true + ); + }); + + it('should handle one number without decimal point', () => { + assert.strictEqual( + compareWithPrecision('123', '123.000', 3), + true + ); + }); + + it('should match Redis coordinates with different precision', () => { + assert.strictEqual( + compareWithPrecision( + '-122.06429868936538696', + '-122.06429868936539', + 13 + ), + true + ); + }); + + it('should match Redis latitude with different precision', () => { + assert.strictEqual( + compareWithPrecision( + '37.37749628831998194', + '37.37749628831998', + 14 + ), + true + ); + }); +}); + +export const compareWithPrecision = (actual: string, expected: string, decimals: number): boolean => { + return Math.abs(Number(actual) - Number(expected)) < Math.pow(10, -decimals); +}; diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index 2d561dd2e20..dce1f97d88a 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -5,10 +5,10 @@ import { CredentialsProvider } from './authx'; import { Command } from './RESP/types'; import { BasicCommandParser } from './client/parser'; -const utils = new TestUtils({ - dockerImageName: 'redis/redis-stack', +const utils = TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '7.4.0-v1' + defaultDockerVersion: '8.0-M04-pre' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index eecec2d4d6d..970637a1225 100644 --- a/packages/entraid/lib/test-utils.ts +++ b/packages/entraid/lib/test-utils.ts @@ -3,10 +3,10 @@ import { IdentityProvider, StreamingCredentialsProvider, TokenManager, TokenResp import TestUtils from '@redis/test-utils'; import { EntraidCredentialsProvider } from './entraid-credentials-provider'; -export const testUtils = new TestUtils({ - dockerImageName: 'redis/redis-stack', +export const testUtils = TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '7.4.0-v1' + defaultDockerVersion: '8.0-M04-pre' }); const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? diff --git a/packages/graph/lib/test-utils.ts b/packages/graph/lib/test-utils.ts index 2aa9384dbe6..16c44582061 100644 --- a/packages/graph/lib/test-utils.ts +++ b/packages/graph/lib/test-utils.ts @@ -1,10 +1,11 @@ import TestUtils from '@redis/test-utils'; import RedisGraph from '.'; -export default new TestUtils({ - dockerImageName: 'redis/redis-stack', - dockerImageVersionArgument: 'redisgraph-version', - defaultDockerVersion: '7.4.0-v1' + +export default TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M04-pre' }); export const GLOBAL = { diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index 0ac30c521b2..caa1c3049af 100644 --- a/packages/json/lib/test-utils.ts +++ b/packages/json/lib/test-utils.ts @@ -1,10 +1,10 @@ import TestUtils from '@redis/test-utils'; import RedisJSON from '.'; -export default new TestUtils({ - dockerImageName: 'redis/redis-stack', - dockerImageVersionArgument: 'redisgraph-version', - defaultDockerVersion: '7.4.0-v1' +export default TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M04-pre' }); export const GLOBAL = { diff --git a/packages/search/lib/commands/CONFIG_SET.spec.ts b/packages/search/lib/commands/CONFIG_SET.spec.ts index 71a4e69f26f..c5922a28756 100644 --- a/packages/search/lib/commands/CONFIG_SET.spec.ts +++ b/packages/search/lib/commands/CONFIG_SET.spec.ts @@ -17,4 +17,40 @@ describe('FT.CONFIG SET', () => { 'OK' ); }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'setSearchConfigGloballyTest', async client => { + + const normalizeObject = obj => JSON.parse(JSON.stringify(obj)); + assert.equal(await client.configSet('search-default-dialect', '3'), + 'OK', 'CONFIG SET should return OK'); + + assert.deepEqual( + normalizeObject(await client.configGet('search-default-dialect')), + { 'search-default-dialect': '3' }, + 'CONFIG GET should return 3' + ); + + assert.deepEqual( + normalizeObject(await client.ft.configGet('DEFAULT_DIALECT')), + { 'DEFAULT_DIALECT': '3' }, + 'FT.CONFIG GET should return 3' + ); + + const ftConfigSetResult = await client.ft.configSet('DEFAULT_DIALECT', '2'); + assert.equal(normalizeObject(ftConfigSetResult), 'OK', 'FT.CONFIG SET should return OK'); + + assert.deepEqual( + normalizeObject(await client.ft.configGet('DEFAULT_DIALECT')), + { 'DEFAULT_DIALECT': '2' }, + 'FT.CONFIG GET should return 2' + ); + + assert.deepEqual( + normalizeObject(await client.configGet('search-default-dialect')), + { 'search-default-dialect': '2' }, + 'CONFIG GET should return 22' + ); + + }, GLOBAL.SERVERS.OPEN); + }); diff --git a/packages/search/lib/commands/INFO.spec.ts b/packages/search/lib/commands/INFO.spec.ts index cbb4ea91677..caf311e07e9 100644 --- a/packages/search/lib/commands/INFO.spec.ts +++ b/packages/search/lib/commands/INFO.spec.ts @@ -5,105 +5,305 @@ import { SCHEMA_FIELD_TYPE } from './CREATE'; import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; describe('INFO', () => { - it('transformArguments', () => { - assert.deepEqual( - parseArgs(INFO, 'index'), - ['FT.INFO', 'index'] - ); + it('transformArguments', () => { + assert.deepEqual( + parseArgs(INFO, 'index'), + ['FT.INFO', 'index'] + ); + }); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.info', async client => { + + await client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT }); + const ret = await client.ft.info('index'); + // effectively testing that stopwords_list is not in ret + assert.deepEqual( + ret, + { + index_name: 'index', + index_options: [], + index_definition: Object.create(null, { + + indexes_all: { + value: 'false', + configurable: true, + enumerable: true + }, + + default_score: { + value: '1', + configurable: true, + enumerable: true + }, + key_type: { + value: 'HASH', + configurable: true, + enumerable: true + }, + prefixes: { + value: [''], + configurable: true, + enumerable: true + } + }), + attributes: [Object.create(null, { + identifier: { + value: 'field', + configurable: true, + enumerable: true + }, + attribute: { + value: 'field', + configurable: true, + enumerable: true + }, + type: { + value: 'TEXT', + configurable: true, + enumerable: true + }, + WEIGHT: { + value: '1', + configurable: true, + enumerable: true + } + })], + num_docs: 0, + max_doc_id: 0, + num_terms: 0, + num_records: 0, + inverted_sz_mb: 0, + vector_index_sz_mb: 0, + total_inverted_index_blocks: 0, + offset_vectors_sz_mb: 0, + doc_table_size_mb: 0, + sortable_values_size_mb: 0, + key_table_size_mb: 0, + records_per_doc_avg: NaN, + bytes_per_record_avg: NaN, + cleaning: 0, + offsets_per_term_avg: NaN, + offset_bits_per_record_avg: NaN, + geoshapes_sz_mb: 0, + hash_indexing_failures: 0, + indexing: 0, + percent_indexed: 1, + number_of_uses: 1, + tag_overhead_sz_mb: 0, + text_overhead_sz_mb: 0, + total_index_memory_sz_mb: 0, + total_indexing_time: 0, + gc_stats: { + bytes_collected: 0, + total_ms_run: 0, + total_cycles: 0, + average_cycle_time_ms: NaN, + last_run_time_ms: 0, + gc_numeric_trees_missed: 0, + gc_blocks_denied: 0 + }, + cursor_stats: { + global_idle: 0, + global_total: 0, + index_capacity: 128, + index_total: 0 + }, + } + ); + + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[7, 4, 2], [7, 4, 2]], 'client.ft.info', async client => { - testUtils.testWithClient('client.ft.info', async client => { - await client.ft.create('index', { - field: SCHEMA_FIELD_TYPE.TEXT - }); - const ret = await client.ft.info('index'); - // effectively testing that stopwords_list is not in ret - assert.deepEqual( - ret, - { - index_name: 'index', - index_options: [], - index_definition: Object.create(null, { - default_score: { - value: '1', - configurable: true, - enumerable: true - }, - key_type: { - value: 'HASH', - configurable: true, - enumerable: true - }, - prefixes: { - value: [''], - configurable: true, - enumerable: true - } - }), - attributes: [Object.create(null, { - identifier: { - value: 'field', - configurable: true, - enumerable: true - }, - attribute: { - value: 'field', - configurable: true, - enumerable: true - }, - type: { - value: 'TEXT', - configurable: true, - enumerable: true - }, - WEIGHT: { - value: '1', - configurable: true, - enumerable: true - } - })], - num_docs: 0, - max_doc_id: 0, - num_terms: 0, - num_records: 0, - inverted_sz_mb: 0, - vector_index_sz_mb: 0, - total_inverted_index_blocks: 0, - offset_vectors_sz_mb: 0, - doc_table_size_mb: 0, - sortable_values_size_mb: 0, - key_table_size_mb: 0, - records_per_doc_avg: NaN, - bytes_per_record_avg: NaN, - cleaning: 0, - offsets_per_term_avg: NaN, - offset_bits_per_record_avg: NaN, - geoshapes_sz_mb: 0, - hash_indexing_failures: 0, - indexing: 0, - percent_indexed: 1, - number_of_uses: 1, - tag_overhead_sz_mb: 0, - text_overhead_sz_mb: 0, - total_index_memory_sz_mb: 0, - total_indexing_time: 0, - gc_stats: { - bytes_collected: 0, - total_ms_run: 0, - total_cycles: 0, - average_cycle_time_ms: NaN, - last_run_time_ms: 0, - gc_numeric_trees_missed: 0, - gc_blocks_denied: 0 - }, - cursor_stats: { - global_idle: 0, - global_total: 0, - index_capacity: 128, - index_total: 0 - }, - } - ); + await client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }); + const ret = await client.ft.info('index'); + // effectively testing that stopwords_list is not in ret + assert.deepEqual( + ret, + { + index_name: 'index', + index_options: [], + index_definition: Object.create(null, { + default_score: { + value: '1', + configurable: true, + enumerable: true + }, + key_type: { + value: 'HASH', + configurable: true, + enumerable: true + }, + prefixes: { + value: [''], + configurable: true, + enumerable: true + } + }), + attributes: [Object.create(null, { + identifier: { + value: 'field', + configurable: true, + enumerable: true + }, + attribute: { + value: 'field', + configurable: true, + enumerable: true + }, + type: { + value: 'TEXT', + configurable: true, + enumerable: true + }, + WEIGHT: { + value: '1', + configurable: true, + enumerable: true + } + })], + num_docs: 0, + max_doc_id: 0, + num_terms: 0, + num_records: 0, + inverted_sz_mb: 0, + vector_index_sz_mb: 0, + total_inverted_index_blocks: 0, + offset_vectors_sz_mb: 0, + doc_table_size_mb: 0, + sortable_values_size_mb: 0, + key_table_size_mb: 0, + records_per_doc_avg: NaN, + bytes_per_record_avg: NaN, + cleaning: 0, + offsets_per_term_avg: NaN, + offset_bits_per_record_avg: NaN, + geoshapes_sz_mb: 0, + hash_indexing_failures: 0, + indexing: 0, + percent_indexed: 1, + number_of_uses: 1, + tag_overhead_sz_mb: 0, + text_overhead_sz_mb: 0, + total_index_memory_sz_mb: 0, + total_indexing_time: 0, + gc_stats: { + bytes_collected: 0, + total_ms_run: 0, + total_cycles: 0, + average_cycle_time_ms: NaN, + last_run_time_ms: 0, + gc_numeric_trees_missed: 0, + gc_blocks_denied: 0 + }, + cursor_stats: { + global_idle: 0, + global_total: 0, + index_capacity: 128, + index_total: 0 + }, + } + ); + + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 2, 0]], 'client.ft.info', async client => { + + await client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.TEXT + }); + const ret = await client.ft.info('index'); + // effectively testing that stopwords_list is not in ret + assert.deepEqual( + ret, + { + index_name: 'index', + index_options: [], + index_definition: Object.create(null, { + default_score: { + value: '1', + configurable: true, + enumerable: true + }, + key_type: { + value: 'HASH', + configurable: true, + enumerable: true + }, + prefixes: { + value: [''], + configurable: true, + enumerable: true + } + }), + attributes: [Object.create(null, { + identifier: { + value: 'field', + configurable: true, + enumerable: true + }, + attribute: { + value: 'field', + configurable: true, + enumerable: true + }, + type: { + value: 'TEXT', + configurable: true, + enumerable: true + }, + WEIGHT: { + value: '1', + configurable: true, + enumerable: true + } + })], + num_docs: "0", + max_doc_id: "0", + num_terms: "0", + num_records: "0", + inverted_sz_mb: 0, + vector_index_sz_mb: 0, + total_inverted_index_blocks: "0", + offset_vectors_sz_mb: 0, + doc_table_size_mb: 0, + sortable_values_size_mb: 0, + key_table_size_mb: 0, + records_per_doc_avg: NaN, + bytes_per_record_avg: NaN, + cleaning: 0, + offsets_per_term_avg: NaN, + offset_bits_per_record_avg: NaN, + geoshapes_sz_mb: 0, + hash_indexing_failures: "0", + indexing: "0", + percent_indexed: 1, + number_of_uses: 1, + tag_overhead_sz_mb: 0, + text_overhead_sz_mb: 0, + total_index_memory_sz_mb: 0, + total_indexing_time: 0, + gc_stats: { + bytes_collected: 0, + total_ms_run: 0, + total_cycles: 0, + average_cycle_time_ms: NaN, + last_run_time_ms: 0, + gc_numeric_trees_missed: 0, + gc_blocks_denied: 0 + }, + cursor_stats: { + global_idle: 0, + global_total: 0, + index_capacity: 128, + index_total: 0 + }, + } + ); - }, GLOBAL.SERVERS.OPEN); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts index 5cfa0500a0a..bdf452c16ea 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts @@ -7,43 +7,106 @@ import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; import { DEFAULT_DIALECT } from '../dialect/default'; describe('PROFILE AGGREGATE', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - parseArgs(PROFILE_AGGREGATE, 'index', 'query'), - ['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT] - ); - }); - - it('with options', () => { - assert.deepEqual( - parseArgs(PROFILE_AGGREGATE, 'index', 'query', { - LIMITED: true, - VERBATIM: true, - STEPS: [{ - type: FT_AGGREGATE_STEPS.SORTBY, - BY: '@by' - }] - }), - ['FT.PROFILE', 'index', 'AGGREGATE', 'LIMITED', 'QUERY', 'query', - 'VERBATIM', 'SORTBY', '1', '@by', 'DIALECT', DEFAULT_DIALECT] - ); - }); + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(PROFILE_AGGREGATE, 'index', 'query'), + ['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT] + ); }); - testUtils.testWithClient('client.ft.search', async client => { - await Promise.all([ - client.ft.create('index', { - field: SCHEMA_FIELD_TYPE.NUMERIC - }), - client.hSet('1', 'field', '1'), - client.hSet('2', 'field', '2') - ]); - - const res = await client.ft.profileAggregate('index', '*'); - assert.deepEqual('None', res.profile.warning); - assert.ok(typeof res.profile.iteratorsProfile.counter === 'number'); - assert.ok(typeof res.profile.parsingTime === 'string'); - assert.ok(res.results.total == 1); - }, GLOBAL.SERVERS.OPEN); + it('with options', () => { + assert.deepEqual( + parseArgs(PROFILE_AGGREGATE, 'index', 'query', { + LIMITED: true, + VERBATIM: true, + STEPS: [{ + type: FT_AGGREGATE_STEPS.SORTBY, + BY: '@by' + }] + }), + ['FT.PROFILE', 'index', 'AGGREGATE', 'LIMITED', 'QUERY', 'query', + 'VERBATIM', 'SORTBY', '1', '@by', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.search', async client => { + await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.NUMERIC + }), + client.hSet('1', 'field', '1'), + client.hSet('2', 'field', '2') + ]); + + + const normalizeObject = obj => JSON.parse(JSON.stringify(obj)); + const res = await client.ft.profileAggregate('index', '*'); + + const normalizedRes = normalizeObject(res); + assert.equal(normalizedRes.results.total, 1); + + assert.ok(normalizedRes.profile[0] === 'Shards'); + assert.ok(Array.isArray(normalizedRes.profile[1])); + assert.ok(normalizedRes.profile[2] === 'Coordinator'); + assert.ok(Array.isArray(normalizedRes.profile[3])); + + const shardProfile = normalizedRes.profile[1][0]; + assert.ok(shardProfile.includes('Total profile time')); + assert.ok(shardProfile.includes('Parsing time')); + assert.ok(shardProfile.includes('Pipeline creation time')); + assert.ok(shardProfile.includes('Warning')); + assert.ok(shardProfile.includes('Iterators profile')); + + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 4, 0]], 'client.ft.search', async client => { + await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.NUMERIC + }), + client.hSet('1', 'field', '1'), + client.hSet('2', 'field', '2') + ]); + + const normalizeObject = obj => JSON.parse(JSON.stringify(obj)); + const res = await client.ft.profileAggregate('index', '*'); + const normalizedRes = normalizeObject(res); + assert.equal(normalizedRes.results.total, 1); + + assert.ok(Array.isArray(normalizedRes.profile)); + assert.equal(normalizedRes.profile[0][0], 'Total profile time'); + assert.equal(normalizedRes.profile[1][0], 'Parsing time'); + assert.equal(normalizedRes.profile[2][0], 'Pipeline creation time'); + assert.equal(normalizedRes.profile[3][0], 'Warning'); + assert.equal(normalizedRes.profile[4][0], 'Iterators profile'); + assert.equal(normalizedRes.profile[5][0], 'Result processors profile'); + + const iteratorsProfile = normalizedRes.profile[4][1]; + assert.equal(iteratorsProfile[0], 'Type'); + assert.equal(iteratorsProfile[1], 'WILDCARD'); + assert.equal(iteratorsProfile[2], 'Time'); + assert.equal(iteratorsProfile[4], 'Counter'); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], '[RESP3] client.ft.search', async client => { + await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.NUMERIC + }), + client.hSet('1', 'field', '1'), + client.hSet('2', 'field', '2') + ]); + + + const normalizeObject = obj => JSON.parse(JSON.stringify(obj)); + const res = await client.ft.profileAggregate('index', '*'); + + const normalizedRes = normalizeObject(res); + assert.equal(normalizedRes.Results.total_results, 1); + assert.ok(normalizedRes.Profile.Shards); + + }, GLOBAL.SERVERS.OPEN_3) + }); diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.ts index ad671c9eb45..94bb6984afa 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.ts @@ -1,37 +1,35 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; -import { Command, ReplyUnion } from "@redis/client/dist/lib/RESP/types"; -import AGGREGATE, { AggregateRawReply, FtAggregateOptions, parseAggregateOptions } from "./AGGREGATE"; -import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from "./PROFILE_SEARCH"; +import { Command, ReplyUnion, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import AGGREGATE, { AggregateRawReply, FtAggregateOptions, parseAggregateOptions } from './AGGREGATE'; +import { ProfileOptions, ProfileRawReplyResp2, ProfileReplyResp2, } from './PROFILE_SEARCH'; export default { NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, - parseCommand( - parser: CommandParser, - index: string, - query: string, - options?: ProfileOptions & FtAggregateOptions - ) { - parser.push('FT.PROFILE', index, 'AGGREGATE'); - - if (options?.LIMITED) { - parser.push('LIMITED'); - } - - parser.push('QUERY', query); + IS_READ_ONLY: true, + parseCommand( + parser: CommandParser, + index: string, + query: string, + options?: ProfileOptions & FtAggregateOptions + ) { + parser.push('FT.PROFILE', index, 'AGGREGATE'); - parseAggregateOptions(parser, options) - }, - transformReply: { - 2: (reply: ProfileAggeregateRawReply): ProfileReply => { - return { - results: AGGREGATE.transformReply[2](reply[0]), - profile: transformProfile(reply[1]) - } - }, - 3: undefined as unknown as () => ReplyUnion - }, - unstableResp3: true - } as const satisfies Command; + if (options?.LIMITED) { + parser.push('LIMITED'); + } - type ProfileAggeregateRawReply = ProfileRawReply; \ No newline at end of file + parser.push('QUERY', query); + + parseAggregateOptions(parser, options) + }, + transformReply: { + 2: (reply: UnwrapReply>): ProfileReplyResp2 => { + return { + results: AGGREGATE.transformReply[2](reply[0]), + profile: reply[1] + } + }, + 3: (reply: ReplyUnion): ReplyUnion => reply + }, + unstableResp3: true +} as const satisfies Command; diff --git a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts index 60f1e8b7474..419b879d00a 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.spec.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.spec.ts @@ -6,39 +6,90 @@ import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; import { DEFAULT_DIALECT } from '../dialect/default'; describe('PROFILE SEARCH', () => { - describe('transformArguments', () => { - it('without options', () => { - assert.deepEqual( - parseArgs(PROFILE_SEARCH, 'index', 'query'), - ['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT] - ); - }); - - it('with options', () => { - assert.deepEqual( - parseArgs(PROFILE_SEARCH, 'index', 'query', { - LIMITED: true, - VERBATIM: true, - INKEYS: 'key' - }), - ['FT.PROFILE', 'index', 'SEARCH', 'LIMITED', 'QUERY', 'query', - 'VERBATIM', 'INKEYS', '1', 'key', 'DIALECT', DEFAULT_DIALECT] - ); - }); + describe('transformArguments', () => { + it('without options', () => { + assert.deepEqual( + parseArgs(PROFILE_SEARCH, 'index', 'query'), + ['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query', 'DIALECT', DEFAULT_DIALECT] + ); }); - testUtils.testWithClient('client.ft.search', async client => { - await Promise.all([ - client.ft.create('index', { - field: SCHEMA_FIELD_TYPE.NUMERIC - }), - client.hSet('1', 'field', '1') - ]); - - const res = await client.ft.profileSearch('index', '*'); - assert.strictEqual('None', res.profile.warning); - assert.ok(typeof res.profile.iteratorsProfile.counter === 'number'); - assert.ok(typeof res.profile.parsingTime === 'string'); - assert.ok(res.results.total == 1); - }, GLOBAL.SERVERS.OPEN); + it('with options', () => { + assert.deepEqual( + parseArgs(PROFILE_SEARCH, 'index', 'query', { + LIMITED: true, + VERBATIM: true, + INKEYS: 'key' + }), + ['FT.PROFILE', 'index', 'SEARCH', 'LIMITED', 'QUERY', 'query', + 'VERBATIM', 'INKEYS', '1', 'key', 'DIALECT', DEFAULT_DIALECT] + ); + }); + }); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.search', async client => { + await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.NUMERIC + }), + client.hSet('1', 'field', '1') + ]); + + const normalizeObject = obj => JSON.parse(JSON.stringify(obj)); + + const res = await client.ft.profileSearch('index', '*'); + + const normalizedRes = normalizeObject(res); + assert.equal(normalizedRes.results.total, 1); + + assert.ok(normalizedRes.profile[0] === 'Shards'); + assert.ok(Array.isArray(normalizedRes.profile[1])); + assert.ok(normalizedRes.profile[2] === 'Coordinator'); + assert.ok(Array.isArray(normalizedRes.profile[3])); + + const shardProfile = normalizedRes.profile[1][0]; + assert.ok(shardProfile.includes('Total profile time')); + assert.ok(shardProfile.includes('Parsing time')); + assert.ok(shardProfile.includes('Pipeline creation time')); + assert.ok(shardProfile.includes('Warning')); + assert.ok(shardProfile.includes('Iterators profile')); + ; + + }, GLOBAL.SERVERS.OPEN); + + + + + + testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 4, 0]], 'client.ft.search', async client => { + await Promise.all([ + client.ft.create('index', { + field: SCHEMA_FIELD_TYPE.NUMERIC + }), + client.hSet('1', 'field', '1') + ]); + + const normalizeObject = obj => JSON.parse(JSON.stringify(obj)); + + const res = await client.ft.profileSearch('index', '*'); + + const normalizedRes = normalizeObject(res); + assert.equal(normalizedRes.results.total, 1); + + assert.ok(Array.isArray(normalizedRes.profile)); + assert.equal(normalizedRes.profile[0][0], 'Total profile time'); + assert.equal(normalizedRes.profile[1][0], 'Parsing time'); + assert.equal(normalizedRes.profile[2][0], 'Pipeline creation time'); + assert.equal(normalizedRes.profile[3][0], 'Warning'); + assert.equal(normalizedRes.profile[4][0], 'Iterators profile'); + assert.equal(normalizedRes.profile[5][0], 'Result processors profile'); + + const iteratorsProfile = normalizedRes.profile[4][1]; + assert.equal(iteratorsProfile[0], 'Type'); + assert.equal(iteratorsProfile[1], 'WILDCARD'); + assert.equal(iteratorsProfile[2], 'Time'); + assert.equal(iteratorsProfile[4], 'Counter'); + + }, GLOBAL.SERVERS.OPEN); + }); diff --git a/packages/search/lib/commands/PROFILE_SEARCH.ts b/packages/search/lib/commands/PROFILE_SEARCH.ts index 86ce85ba7f1..b13dbebe996 100644 --- a/packages/search/lib/commands/PROFILE_SEARCH.ts +++ b/packages/search/lib/commands/PROFILE_SEARCH.ts @@ -1,23 +1,19 @@ import { CommandParser } from '@redis/client/dist/lib/client/parser'; -import { Command, RedisArgument, ReplyUnion } from "@redis/client/dist/lib/RESP/types"; -import { AggregateReply } from "./AGGREGATE"; -import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, parseSearchOptions } from "./SEARCH"; +import { ArrayReply, Command, RedisArgument, ReplyUnion, TuplesReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types'; +import { AggregateReply } from './AGGREGATE'; +import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, parseSearchOptions } from './SEARCH'; -export type ProfileRawReply = [ - results: T, - profile: [ - _: string, - TotalProfileTime: string, - _: string, - ParsingTime: string, - _: string, - PipelineCreationTime: string, - _: string, - IteratorsProfile: Array - ] -]; +export type ProfileRawReplyResp2 = TuplesReply<[ + T, + ArrayReply +]>; -type ProfileSearchRawReply = ProfileRawReply; +type ProfileSearchResponseResp2 = ProfileRawReplyResp2; + +export interface ProfileReplyResp2 { + results: SearchReply | AggregateReply; + profile: ReplyUnion; +} export interface ProfileOptions { LIMITED?: true; @@ -43,108 +39,13 @@ export default { parseSearchOptions(parser, options); }, transformReply: { - 2: (reply: ProfileSearchRawReply, withoutDocuments: boolean): ProfileReply => { + 2: (reply: UnwrapReply): ProfileReplyResp2 => { return { results: SEARCH.transformReply[2](reply[0]), - profile: transformProfile(reply[1]) - } + profile: reply[1] + }; }, - 3: undefined as unknown as () => ReplyUnion + 3: (reply: ReplyUnion): ReplyUnion => reply }, unstableResp3: true } as const satisfies Command; - -export interface ProfileReply { - results: SearchReply | AggregateReply; - profile: ProfileData; -} - -interface ChildIterator { - type?: string, - counter?: number, - term?: string, - size?: number, - time?: string, - childIterators?: Array -} - -interface IteratorsProfile { - type?: string, - counter?: number, - queryType?: string, - time?: string, - childIterators?: Array -} - -interface ProfileData { - totalProfileTime: string, - parsingTime: string, - pipelineCreationTime: string, - warning: string, - iteratorsProfile: IteratorsProfile -} - -export function transformProfile(reply: Array): ProfileData{ - return { - totalProfileTime: reply[0][1], - parsingTime: reply[1][1], - pipelineCreationTime: reply[2][1], - warning: reply[3][1] ? reply[3][1] : 'None', - iteratorsProfile: transformIterators(reply[4][1]) - }; -} - -function transformIterators(IteratorsProfile: Array): IteratorsProfile { - var res: IteratorsProfile = {}; - for (let i = 0; i < IteratorsProfile.length; i += 2) { - const value = IteratorsProfile[i+1]; - switch (IteratorsProfile[i]) { - case 'Type': - res.type = value; - break; - case 'Counter': - res.counter = value; - break; - case 'Time': - res.time = value; - break; - case 'Query type': - res.queryType = value; - break; - case 'Child iterators': - res.childIterators = value.map(transformChildIterators); - break; - } - } - - return res; -} - -function transformChildIterators(IteratorsProfile: Array): ChildIterator { - var res: ChildIterator = {}; - for (let i = 1; i < IteratorsProfile.length; i += 2) { - const value = IteratorsProfile[i+1]; - switch (IteratorsProfile[i]) { - case 'Type': - res.type = value; - break; - case 'Counter': - res.counter = value; - break; - case 'Time': - res.time = value; - break; - case 'Size': - res.size = value; - break; - case 'Term': - res.term = value; - break; - case 'Child iterators': - res.childIterators = value.map(transformChildIterators); - break; - } - } - - return res; -} \ No newline at end of file diff --git a/packages/search/lib/commands/SUGGET.spec.ts b/packages/search/lib/commands/SUGGET.spec.ts index e30c62afd67..b82ea547782 100644 --- a/packages/search/lib/commands/SUGGET.spec.ts +++ b/packages/search/lib/commands/SUGGET.spec.ts @@ -28,13 +28,23 @@ describe('FT.SUGGET', () => { }); describe('client.ft.sugGet', () => { - testUtils.testWithClient('null', async client => { - assert.equal( + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => { + assert.deepStrictEqual( await client.ft.sugGet('key', 'prefix'), - null + [] ); }, GLOBAL.SERVERS.OPEN); + + + testUtils.testWithClientIfVersionWithinRange([[6, 2, 0], [7, 4, 0]], 'null', async client => { + assert.deepStrictEqual( + await client.ft.sugGet('key', 'prefix'), + null + ); + }, GLOBAL.SERVERS.OPEN) + testUtils.testWithClient('with suggestions', async client => { const [, reply] = await Promise.all([ client.ft.sugAdd('key', 'string', 1), diff --git a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts index 160d7e3eb7c..c01b87e2892 100644 --- a/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHPAYLOADS.spec.ts @@ -11,14 +11,21 @@ describe('FT.SUGGET WITHPAYLOADS', () => { ); }); - describe('client.ft.sugGetWithPayloads', () => { - testUtils.testWithClient('null', async client => { - assert.equal( - await client.ft.sugGetWithPayloads('key', 'prefix'), - null - ); - }, GLOBAL.SERVERS.OPEN); + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => { + assert.deepStrictEqual( + await client.ft.sugGetWithPayloads('key', 'prefix'), + [] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[6], [7, 4, 0]], 'null', async client => { + assert.deepStrictEqual( + await client.ft.sugGetWithPayloads('key', 'prefix'), + null + ); + }, GLOBAL.SERVERS.OPEN); + describe('with suggestions', () => { testUtils.testWithClient('with suggestions', async client => { const [, reply] = await Promise.all([ client.ft.sugAdd('key', 'string', 1, { diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts b/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts index 262defb7933..50db89ffe99 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES.spec.ts @@ -12,14 +12,15 @@ describe('FT.SUGGET WITHSCORES', () => { }); describe('client.ft.sugGetWithScores', () => { - testUtils.testWithClient('null', async client => { - assert.equal( + + testUtils.testWithClientIfVersionWithinRange([[8],'LATEST'], 'null', async client => { + assert.deepStrictEqual( await client.ft.sugGetWithScores('key', 'prefix'), - null + [] ); }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with suggestions', async client => { + testUtils.testWithClientIfVersionWithinRange([[8],'LATEST'],'with suggestions', async client => { const [, reply] = await Promise.all([ client.ft.sugAdd('key', 'string', 1), client.ft.sugGetWithScores('key', 's') diff --git a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts index 573708f689e..96eb473159f 100644 --- a/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts +++ b/packages/search/lib/commands/SUGGET_WITHSCORES_WITHPAYLOADS.spec.ts @@ -12,14 +12,14 @@ describe('FT.SUGGET WITHSCORES WITHPAYLOADS', () => { }); describe('client.ft.sugGetWithScoresWithPayloads', () => { - testUtils.testWithClient('null', async client => { - assert.equal( + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'null', async client => { + assert.deepStrictEqual( await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'), - null + [] ); }, GLOBAL.SERVERS.OPEN); - testUtils.testWithClient('with suggestions', async client => { + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'with suggestions', async client => { const [, reply] = await Promise.all([ client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' diff --git a/packages/search/lib/commands/index.ts b/packages/search/lib/commands/index.ts index 00706a70c2e..7aa3f061bf7 100644 --- a/packages/search/lib/commands/index.ts +++ b/packages/search/lib/commands/index.ts @@ -48,9 +48,21 @@ export default { aliasDel: ALIASDEL, ALIASUPDATE, aliasUpdate: ALIASUPDATE, + /** + * @deprecated Redis >=8 uses the standard CONFIG command + */ CONFIG_GET, + /** + * @deprecated Redis >=8 uses the standard CONFIG command + */ configGet: CONFIG_GET, + /** + * @deprecated Redis >=8 uses the standard CONFIG command + */ CONFIG_SET, + /** + * @deprecated Redis >=8 uses the standard CONFIG command + */ configSet: CONFIG_SET, CREATE, create: CREATE, diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index ce43a37bc21..1318676042e 100644 --- a/packages/search/lib/test-utils.ts +++ b/packages/search/lib/test-utils.ts @@ -1,21 +1,32 @@ import TestUtils from '@redis/test-utils'; import RediSearch from '.'; +import { RespVersions } from '@redis/client'; -export default new TestUtils({ - dockerImageName: 'redis/redis-stack', - dockerImageVersionArgument: 'redisearch-version', - defaultDockerVersion: '7.4.0-v1' +export default TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M04-pre' }); export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: [], - clientOptions: { - modules: { - ft: RediSearch - } - } + SERVERS: { + OPEN: { + serverArguments: [], + clientOptions: { + modules: { + ft: RediSearch } + } + }, + OPEN_3: { + serverArguments: [], + clientOptions: { + RESP: 3 as RespVersions, + unstableResp3:true, + modules: { + ft: RediSearch + } + } } + } }; diff --git a/packages/search/package.json b/packages/search/package.json index 26cbbaf5ad9..985356cd230 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -9,7 +9,8 @@ "!dist/tsconfig.tsbuildinfo" ], "scripts": { - "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'", + "test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'" }, "peerDependencies": { "@redis/client": "^5.0.0-next.6" diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index bfb66603750..e3ff5edc38b 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -4,9 +4,11 @@ import { once } from 'node:events'; import { createClient } from '@redis/client/index'; import { setTimeout } from 'node:timers/promises'; // import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS'; + +import { execFile as execFileCallback } from 'node:child_process'; import { promisify } from 'node:util'; -import { exec } from 'node:child_process'; -const execAsync = promisify(exec); + +const execAsync = promisify(execFileCallback); interface ErrorWithCode extends Error { code: string; @@ -46,11 +48,29 @@ export interface RedisServerDocker { dockerId: string; } -async function spawnRedisServerDocker({ image, version }: RedisServerDockerConfig, serverArguments: Array): Promise { - const port = (await portIterator.next()).value, - { stdout, stderr } = await execAsync( - `docker run -e REDIS_ARGS="--port ${port.toString()} ${serverArguments.join(' ')}" -d --network host ${image}:${version}` - ); +async function spawnRedisServerDocker({ + image, + version +}: RedisServerDockerConfig, serverArguments: Array): Promise { + const port = (await portIterator.next()).value; + const portStr = port.toString(); + + const dockerArgs = [ + 'run', + '-e', `PORT=${portStr}`, + '-d', + '--network', 'host', + `${image}:${version}`, + '--port', portStr + ]; + + if (serverArguments.length > 0) { + dockerArgs.push(...serverArguments); + } + + console.log(`[Docker] Spawning Redis container - Image: ${image}:${version}, Port: ${port}`); + + const { stdout, stderr } = await execAsync('docker', dockerArgs); if (!stdout) { throw new Error(`docker run error - ${stderr}`); @@ -65,7 +85,6 @@ async function spawnRedisServerDocker({ image, version }: RedisServerDockerConfi dockerId: stdout.trim() }; } - const RUNNING_SERVERS = new Map, ReturnType>(); export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverArguments: Array): Promise { @@ -80,7 +99,7 @@ export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverAr } async function dockerRemove(dockerId: string): Promise { - const { stderr } = await execAsync(`docker rm -f ${dockerId}`); + const { stderr } = await execAsync('docker', ['rm', '-f', dockerId]); if (stderr) { throw new Error(`docker rm error - ${stderr}`); } @@ -132,15 +151,15 @@ async function spawnRedisClusterNodeDockers( '5000' ], clientConfig).then(async replica => { - const requirePassIndex = serverArguments.findIndex((x)=>x==='--requirepass'); - if(requirePassIndex!==-1) { - const password = serverArguments[requirePassIndex+1]; - await replica.client.configSet({'masterauth': password}) + const requirePassIndex = serverArguments.findIndex((x) => x === '--requirepass'); + if (requirePassIndex !== -1) { + const password = serverArguments[requirePassIndex + 1]; + await replica.client.configSet({ 'masterauth': password }) } await replica.client.clusterMeet('127.0.0.1', master.docker.port); while ((await replica.client.clusterSlots()).length === 0) { - await setTimeout(50); + await setTimeout(25); } await replica.client.clusterReplicate( @@ -224,7 +243,7 @@ async function spawnRedisClusterDockers( while ( totalNodes(await client.clusterSlots()) !== nodes.length || !(await client.sendCommand(['CLUSTER', 'INFO'])).startsWith('cluster_state:ok') // TODO - ) { + ) { await setTimeout(50); } @@ -257,7 +276,7 @@ export function spawnRedisCluster( return runningCluster; } - const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments,clientConfig); + const dockersPromise = spawnRedisClusterDockers(dockersConfig, serverArguments, clientConfig); RUNNING_CLUSTERS.set(serverArguments, dockersPromise); return dockersPromise; diff --git a/packages/test-utils/lib/index.spec.ts b/packages/test-utils/lib/index.spec.ts new file mode 100644 index 00000000000..0f1e7552284 --- /dev/null +++ b/packages/test-utils/lib/index.spec.ts @@ -0,0 +1,106 @@ +import { strict as assert } from 'node:assert'; +import TestUtils from './index'; + +describe('TestUtils', () => { + describe('parseVersionNumber', () => { + it('should handle special versions', () => { + assert.deepStrictEqual(TestUtils.parseVersionNumber('latest'), [Infinity]); + assert.deepStrictEqual(TestUtils.parseVersionNumber('edge'), [Infinity]); + }); + + it('should parse simple version numbers', () => { + assert.deepStrictEqual(TestUtils.parseVersionNumber('7.4.0'), [7, 4, 0]); + }); + + it('should handle versions with multiple dashes and prefixes', () => { + assert.deepStrictEqual(TestUtils.parseVersionNumber('rs-7.4.0-v2'), [7, 4, 0]); + assert.deepStrictEqual(TestUtils.parseVersionNumber('rs-7.4.0'), [7, 4, 0]); + assert.deepStrictEqual(TestUtils.parseVersionNumber('7.4.0-v2'), [7, 4, 0]); + }); + + it('should handle various version number formats', () => { + assert.deepStrictEqual(TestUtils.parseVersionNumber('10.5'), [10, 5]); + assert.deepStrictEqual(TestUtils.parseVersionNumber('8.0.0'), [8, 0, 0]); + assert.deepStrictEqual(TestUtils.parseVersionNumber('rs-6.2.4-v1'), [6, 2, 4]); + }); + + it('should throw TypeError for invalid version strings', () => { + ['', 'invalid', 'rs-', 'v2', 'rs-invalid-v2'].forEach(version => { + assert.throws( + () => TestUtils.parseVersionNumber(version), + TypeError, + `Expected TypeError for version string: ${version}` + ); + }); + }); + }); +}); + + + +describe('Version Comparison', () => { + it('should correctly compare versions', () => { + const tests: [Array, Array, -1 | 0 | 1][] = [ + [[1, 0, 0], [1, 0, 0], 0], + [[2, 0, 0], [1, 9, 9], 1], + [[1, 9, 9], [2, 0, 0], -1], + [[1, 2, 3], [1, 2], 1], + [[1, 2], [1, 2, 3], -1], + [[1, 2, 0], [1, 2, 1], -1], + [[1], [1, 0, 0], 0], + [[2], [1, 9, 9], 1], + ]; + + tests.forEach(([a, b, expected]) => { + + assert.equal( + TestUtils.compareVersions(a, b), + expected, + `Failed comparing ${a.join('.')} with ${b.join('.')}: expected ${expected}` + ); + }); + }); + + it('should correctly compare versions', () => { + const tests: [Array, Array, -1 | 0 | 1][] = [ + [[1, 0, 0], [1, 0, 0], 0], + [[2, 0, 0], [1, 9, 9], 1], + [[1, 9, 9], [2, 0, 0], -1], + [[1, 2, 3], [1, 2], 1], + [[1, 2], [1, 2, 3], -1], + [[1, 2, 0], [1, 2, 1], -1], + [[1], [1, 0, 0], 0], + [[2], [1, 9, 9], 1], + ]; + + tests.forEach(([a, b, expected]) => { + + assert.equal( + TestUtils.compareVersions(a, b), + expected, + `Failed comparing ${a.join('.')} with ${b.join('.')}: expected ${expected}` + ); + }); + }) + it('isVersionInRange should work correctly', () => { + const tests: [Array, Array, Array, boolean][] = [ + [[7, 0, 0], [7, 0, 0], [7, 0, 0], true], + [[7, 0, 1], [7, 0, 0], [7, 0, 2], true], + [[7, 0, 0], [7, 0, 1], [7, 0, 2], false], + [[7, 0, 3], [7, 0, 1], [7, 0, 2], false], + [[7], [6, 0, 0], [8, 0, 0], true], + [[7, 1, 1], [7, 1, 0], [7, 1, 2], true], + [[6, 0, 0], [7, 0, 0], [8, 0, 0], false], + [[9, 0, 0], [7, 0, 0], [8, 0, 0], false] + ]; + + tests.forEach(([version, min, max, expected]) => { + const testUtils = new TestUtils({ string: version.join('.'), numbers: version }, "test") + assert.equal( + testUtils.isVersionInRange(min, max), + expected, + `Failed checking if ${version.join('.')} is between ${min.join('.')} and ${max.join('.')}: expected ${expected}` + ); + }); + }) +}); diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 9dee350e31e..b48f11b02c7 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -19,12 +19,38 @@ import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './ import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; + interface TestUtilsConfig { + /** + * The name of the Docker image to use for spawning Redis test instances. + * This should be a valid Docker image name that contains a Redis server. + * + * @example 'redislabs/client-libs-test' + */ dockerImageName: string; + + /** + * The command-line argument name used to specify the Redis version. + * This argument can be passed when running tests / GH actions. + * + * @example + * If set to 'redis-version', you can run tests with: + * ```bash + * npm test -- --redis-version="6.2" + * ``` + */ dockerImageVersionArgument: string; + + /** + * The default Redis version to use if no version is specified via command-line arguments. + * Can be a specific version number (e.g., '6.2'), 'latest', or 'edge'. + * If not provided, defaults to 'latest'. + * + * @optional + * @default 'latest' + */ defaultDockerVersion?: string; } - interface CommonTestOptions { serverArguments: Array; minimumDockerVersion?: Array; @@ -83,22 +109,27 @@ interface Version { } export default class TestUtils { - static #parseVersionNumber(version: string): Array { + static parseVersionNumber(version: string): Array { if (version === 'latest' || version === 'edge') return [Infinity]; - const dashIndex = version.indexOf('-'); - return (dashIndex === -1 ? version : version.substring(0, dashIndex)) - .split('.') - .map(x => { - const value = Number(x); - if (Number.isNaN(value)) { - throw new TypeError(`${version} is not a valid redis version`); - } - return value; - }); - } + // Match complete version number patterns + const versionMatch = version.match(/(^|\-)\d+(\.\d+)*($|\-)/); + if (!versionMatch) { + throw new TypeError(`${version} is not a valid redis version`); + } + // Extract just the numbers and dots between first and last dash (or start/end) + const versionNumbers = versionMatch[0].replace(/^\-|\-$/g, ''); + + return versionNumbers.split('.').map(x => { + const value = Number(x); + if (Number.isNaN(value)) { + throw new TypeError(`${version} is not a valid redis version`); + } + return value; + }); + } static #getVersion(argumentName: string, defaultVersion = 'latest'): Version { return yargs(hideBin(process.argv)) .option(argumentName, { @@ -108,7 +139,7 @@ export default class TestUtils { .coerce(argumentName, (version: string) => { return { string: version, - numbers: TestUtils.#parseVersionNumber(version) + numbers: TestUtils.parseVersionNumber(version) }; }) .demandOption(argumentName) @@ -118,39 +149,76 @@ export default class TestUtils { readonly #VERSION_NUMBERS: Array; readonly #DOCKER_IMAGE: RedisServerDockerConfig; - constructor(config: TestUtilsConfig) { - const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion); + constructor({ string, numbers }: Version, dockerImageName: string) { this.#VERSION_NUMBERS = numbers; this.#DOCKER_IMAGE = { - image: config.dockerImageName, + image: dockerImageName, version: string }; } + /** + * Creates a new TestUtils instance from a configuration object. + * + * @param config - Configuration object containing Docker image and version settings + * @param config.dockerImageName - The name of the Docker image to use for tests + * @param config.dockerImageVersionArgument - The command-line argument name for specifying Redis version + * @param config.defaultDockerVersion - Optional default Redis version if not specified via arguments + * @returns A new TestUtils instance configured with the provided settings + */ + public static createFromConfig(config: TestUtilsConfig) { + return new TestUtils( + TestUtils.#getVersion(config.dockerImageVersionArgument, + config.defaultDockerVersion), config.dockerImageName); + } + isVersionGreaterThan(minimumVersion: Array | undefined): boolean { if (minimumVersion === undefined) return true; - - const lastIndex = Math.min(this.#VERSION_NUMBERS.length, minimumVersion.length) - 1; - for (let i = 0; i < lastIndex; i++) { - if (this.#VERSION_NUMBERS[i] > minimumVersion[i]) { - return true; - } else if (minimumVersion[i] > this.#VERSION_NUMBERS[i]) { - return false; - } - } - - return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex]; + return TestUtils.compareVersions(this.#VERSION_NUMBERS, minimumVersion) >= 0; } isVersionGreaterThanHook(minimumVersion: Array | undefined): void { - const isVersionGreaterThan = this.isVersionGreaterThan.bind(this); + + const isVersionGreaterThanHook = this.isVersionGreaterThan.bind(this); + const versionNumber = this.#VERSION_NUMBERS.join('.'); + const minimumVersionString = minimumVersion?.join('.'); before(function () { - if (!isVersionGreaterThan(minimumVersion)) { + if (!isVersionGreaterThanHook(minimumVersion)) { + console.warn(`TestUtils: Version ${versionNumber} is less than minimum version ${minimumVersionString}, skipping test`); return this.skip(); } }); } + isVersionInRange(minVersion: Array, maxVersion: Array): boolean { + return TestUtils.compareVersions(this.#VERSION_NUMBERS, minVersion) >= 0 && + TestUtils.compareVersions(this.#VERSION_NUMBERS, maxVersion) <= 0 + } + + /** + * Compares two semantic version arrays and returns: + * -1 if version a is less than version b + * 0 if version a equals version b + * 1 if version a is greater than version b + * + * @param a First version array + * @param b Second version array + * @returns -1 | 0 | 1 + */ + static compareVersions(a: Array, b: Array): -1 | 0 | 1 { + const maxLength = Math.max(a.length, b.length); + + const paddedA = [...a, ...Array(maxLength - a.length).fill(0)]; + const paddedB = [...b, ...Array(maxLength - b.length).fill(0)]; + + for (let i = 0; i < maxLength; i++) { + if (paddedA[i] > paddedB[i]) return 1; + if (paddedA[i] < paddedB[i]) return -1; + } + + return 0; + } + testWithClient< M extends RedisModules = {}, F extends RedisFunctions = {}, @@ -204,6 +272,27 @@ export default class TestUtils { }); } + testWithClientIfVersionWithinRange< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + >( + range: ([minVersion: Array, maxVersion: Array] | [minVersion: Array, 'LATEST']), + title: string, + fn: (client: RedisClientType) => unknown, + options: ClientTestOptions + ): void { + + if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) { + return this.testWithClient(`${title} [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options) + } else { + console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`) + } + + } + testWithClientPool< M extends RedisModules = {}, F extends RedisFunctions = {}, diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 5e291211b6c..f7373f6add1 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -3,6 +3,9 @@ "private": true, "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", + "scripts": { + "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" + }, "peerDependencies": { "@redis/client": "*" }, diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index 1cb5c8ed97b..0b7e940788f 100644 --- a/packages/time-series/lib/test-utils.ts +++ b/packages/time-series/lib/test-utils.ts @@ -1,10 +1,10 @@ import TestUtils from '@redis/test-utils'; import TimeSeries from '.'; -export default new TestUtils({ - dockerImageName: 'redis/redis-stack', - dockerImageVersionArgument: 'timeseries-version', - defaultDockerVersion: '7.4.0-v1' +export default TestUtils.createFromConfig({ + dockerImageName: 'redislabs/client-libs-test', + dockerImageVersionArgument: 'redis-version', + defaultDockerVersion: '8.0-M04-pre' }); export const GLOBAL = { From 8b4ed0059aabc26bde8cc228459be18836cd6f65 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Wed, 5 Mar 2025 14:47:18 +0200 Subject: [PATCH 043/105] feat(entraid): add support for azure identity (#2901) This PR adds support for using Azure Identity's credential classes with Redis Enterprise Entra ID authentication. The main changes include: - Add a new factory method createForDefaultAzureCredential to enable using Azure Identity credentials - Add @azure/identity as a dependency to support the new authentication flow - Add support for DefaultAzureCredential, EnvironmentCredential, and any other TokenCredential implementation - Create a new AzureIdentityProvider to support DefaultAzureCredential - Update documentation and README with usage examples for DefaultAzureCredential - Add integration tests for the new authentication methods - Include a sample application demonstrating interactive browser authentication - Export constants for Redis scopes / credential mappers to simplify authentication configuration --- package-lock.json | 291 +++++++++++++++--- packages/entraid/README.md | 50 +++ .../entraid-integration.spec.ts | 113 +++++-- .../entraid/lib/azure-identity-provider.ts | 22 ++ .../entra-id-credentials-provider-factory.ts | 88 ++++-- .../lib/entraid-credentials-provider.ts | 81 ++++- .../entraid/lib/msal-identity-provider.ts | 20 +- packages/entraid/package.json | 2 + .../samples/interactive-browser/index.ts | 111 +++++++ 9 files changed, 655 insertions(+), 123 deletions(-) create mode 100644 packages/entraid/lib/azure-identity-provider.ts create mode 100644 packages/entraid/samples/interactive-browser/index.ts diff --git a/package-lock.json b/package-lock.json index 8fdd049a5b2..25a1dc9d51c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33,6 +33,214 @@ "node": ">=6.0.0" } }, + "node_modules/@azure/abort-controller": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@azure/abort-controller/-/abort-controller-2.1.2.tgz", + "integrity": "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz", + "integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-util": "^1.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.9.2.tgz", + "integrity": "sha512-kRdry/rav3fUKHl/aDLd/pDLcB+4pOFwPPTVEExuMyaI5r+JBbMWqRbCY1pn5BniDaU3lRxO9eaQ1AmSMehl/w==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.4.0", + "@azure/core-rest-pipeline": "^1.9.1", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.6.1", + "@azure/logger": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.19.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.19.0.tgz", + "integrity": "sha512-bM3308LRyg5g7r3Twprtqww0R/r7+GyVxj4BafcmVPo4WQoGt5JXuaqxHEFjw2o3rvFZcUPiqJMg6WuvEEeVUA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.8.0", + "@azure/core-tracing": "^1.0.1", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.2.0.tgz", + "integrity": "sha512-UKTiEJPkWcESPYJz3X5uKRYyOcJD+4nYph+KpfdPRnQJVrZfk0KJgdnaAWKfhsBBtAf/D58Az4AvCJEmWgIBAg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.11.0.tgz", + "integrity": "sha512-DxOSLua+NdpWoSqULhjDyAZTXFdP/LKkqtYuxxz1SCN289zk3OG8UOpnCQAz/tygyACBtWp/BoO72ptK7msY8g==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.7.0.tgz", + "integrity": "sha512-6z/S2KorkbKaZ0DgZFVRdu7RCuATmMSTjKpuhj7YpjxkJ0vnJ7kTM3cpNgzFgk9OPYfZ31wrBEtC/iwAS4jQDA==", + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.2.1", + "events": "^3.0.0", + "jws": "^4.0.0", + "open": "^10.1.0", + "stoppable": "^1.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/msal-common": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz", + "integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/msal-node": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.2.3.tgz", + "integrity": "sha512-0eaPqBIWEAizeYiXdeHb09Iq0tvHJ17ztvNEaLdr/KcJJhJxbpkkEQf09DB+vKlFE0tzYi7j4rYLTXtES/InEQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.2.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@azure/identity/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/identity/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@azure/identity/node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@azure/logger": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.1.4.tgz", + "integrity": "sha512-4IXXzcCdLdlXuCG+8UKEwLA1T1NHqUfanhXYHiQTn+6sfWCZXduqbtXDGceg3Ce5QxTGo7EqmbV6Bi+aqKuClQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.4.0.tgz", + "integrity": "sha512-rU6juYXk67CKQmpgi6fDgZoPQ9InZ1760z1BSAH7RbeIc4lHZM/Tu+H0CyRk7cnrfvTkexyYE4pjYhMghpzheA==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.2.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-browser/node_modules/@azure/msal-common": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.2.0.tgz", + "integrity": "sha512-HiYfGAKthisUYqHG1nImCf/uzcyS31wng3o+CycWLIM9chnYJ9Lk6jZ30Y6YiYYpTQ9+z/FGUpiKKekd3Arc0A==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@azure/msal-common": { "version": "14.16.0", "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", @@ -847,10 +1055,6 @@ "node": ">=12" } }, - "node_modules/@redis/authx": { - "resolved": "packages/authx", - "link": true - }, "node_modules/@redis/bloom": { "resolved": "packages/bloom", "link": true @@ -1128,7 +1332,6 @@ }, "node_modules/agent-base": { "version": "7.1.0", - "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.4" @@ -1676,7 +1879,6 @@ }, "node_modules/bundle-name": { "version": "4.1.0", - "dev": true, "license": "MIT", "dependencies": { "run-applescript": "^7.0.0" @@ -2161,7 +2363,6 @@ }, "node_modules/debug": { "version": "4.3.4", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -2177,7 +2378,6 @@ }, "node_modules/debug/node_modules/ms": { "version": "2.1.2", - "dev": true, "license": "MIT" }, "node_modules/decamelize": { @@ -2223,7 +2423,6 @@ }, "node_modules/default-browser": { "version": "5.2.1", - "dev": true, "license": "MIT", "dependencies": { "bundle-name": "^4.1.0", @@ -2238,7 +2437,6 @@ }, "node_modules/default-browser-id": { "version": "5.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -2300,7 +2498,6 @@ }, "node_modules/define-lazy-prop": { "version": "3.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -2736,6 +2933,15 @@ "node": ">= 0.6" } }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/execa": { "version": "8.0.1", "dev": true, @@ -3689,7 +3895,6 @@ }, "node_modules/http-proxy-agent": { "version": "7.0.0", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.1.0", @@ -3713,7 +3918,6 @@ }, "node_modules/https-proxy-agent": { "version": "7.0.2", - "dev": true, "license": "MIT", "dependencies": { "agent-base": "^7.0.2", @@ -4087,7 +4291,6 @@ }, "node_modules/is-docker": { "version": "3.0.0", - "dev": true, "license": "MIT", "bin": { "is-docker": "cli.js" @@ -4142,7 +4345,6 @@ }, "node_modules/is-inside-container": { "version": "1.0.0", - "dev": true, "license": "MIT", "dependencies": { "is-docker": "^3.0.0" @@ -4391,7 +4593,6 @@ }, "node_modules/is-wsl": { "version": "3.1.0", - "dev": true, "license": "MIT", "dependencies": { "is-inside-container": "^1.0.0" @@ -6654,7 +6855,6 @@ }, "node_modules/run-applescript": { "version": "7.0.0", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -7146,6 +7346,16 @@ "node": ">= 0.4" } }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "dev": true, @@ -7368,7 +7578,6 @@ }, "node_modules/tslib": { "version": "2.6.2", - "dev": true, "license": "0BSD" }, "node_modules/tsx": { @@ -8129,6 +8338,7 @@ "packages/authx": { "name": "@redis/authx", "version": "5.0.0-next.5", + "extraneous": true, "license": "MIT", "dependencies": { "@azure/msal-node": "^2.16.1" @@ -8143,7 +8353,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8152,12 +8362,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" } }, "packages/client": { "name": "@redis/client", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -8169,16 +8379,14 @@ }, "engines": { "node": ">= 18" - }, - "peerDependencies": { - "@redis/authx": "^5.0.0-next.5" } }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "dependencies": { + "@azure/identity": "4.7.0", "@azure/msal-node": "^2.16.1" }, "devDependencies": { @@ -8194,8 +8402,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/authx": "^5.0.0-next.5", - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" } }, "packages/entraid/node_modules/@types/node": { @@ -8217,7 +8424,7 @@ }, "packages/graph": { "name": "@redis/graph", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8226,12 +8433,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" } }, "packages/json": { "name": "@redis/json", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8240,19 +8447,19 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" } }, "packages/redis": { - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "dependencies": { - "@redis/bloom": "5.0.0-next.5", - "@redis/client": "5.0.0-next.5", - "@redis/graph": "5.0.0-next.5", - "@redis/json": "5.0.0-next.5", - "@redis/search": "5.0.0-next.5", - "@redis/time-series": "5.0.0-next.5" + "@redis/bloom": "5.0.0-next.6", + "@redis/client": "5.0.0-next.6", + "@redis/graph": "5.0.0-next.6", + "@redis/json": "5.0.0-next.6", + "@redis/search": "5.0.0-next.6", + "@redis/time-series": "5.0.0-next.6" }, "engines": { "node": ">= 18" @@ -8260,7 +8467,7 @@ }, "packages/search": { "name": "@redis/search", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8269,7 +8476,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" } }, "packages/test-utils": { @@ -8338,7 +8545,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.0.0-next.5", + "version": "5.0.0-next.6", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8347,7 +8554,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.5" + "@redis/client": "^5.0.0-next.6" } } } diff --git a/packages/entraid/README.md b/packages/entraid/README.md index eec88d71360..f2212848455 100644 --- a/packages/entraid/README.md +++ b/packages/entraid/README.md @@ -11,6 +11,7 @@ Secure token-based authentication for Redis clients using Microsoft Entra ID (fo - Managed identities (system-assigned and user-assigned) - Service principals (with or without certificates) - Authorization Code with PKCE flow + - DefaultAzureCredential from @azure/identity - Built-in retry mechanisms for transient failures ## Installation @@ -30,6 +31,7 @@ The first step to using @redis/entraid is choosing the right credentials provide - `createForClientCredentials`: Use when authenticating with a service principal using client secret - `createForClientCredentialsWithCertificate`: Use when authenticating with a service principal using a certificate - `createForAuthorizationCodeWithPKCE`: Use for interactive authentication flows in user applications +- `createForDefaultAzureCredential`: Use when you want to leverage Azure Identity's DefaultAzureCredential ## Usage Examples @@ -82,6 +84,54 @@ const provider = EntraIdCredentialsProviderFactory.createForUserAssignedManagedI }); ``` +### DefaultAzureCredential Authentication + +tip: see a real sample here: [samples/interactive-browser/index.ts](./samples/interactive-browser/index.ts) + +The DefaultAzureCredential from @azure/identity provides a simplified authentication experience that automatically tries different authentication methods based on the environment. This is especially useful for applications that need to work in different environments (local development, CI/CD, and production). + +```typescript +import { createClient } from '@redis/client'; +import { DefaultAzureCredential } from '@azure/identity'; +import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from '@redis/entraid/dist/lib/entra-id-credentials-provider-factory'; + +// Create a DefaultAzureCredential instance +const credential = new DefaultAzureCredential(); + +// Create a provider using DefaultAzureCredential +const provider = EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({ + // Use the same parameters you would pass to credential.getToken() + credential, + scopes: REDIS_SCOPE_DEFAULT, // The Redis scope + // Optional additional parameters for getToken + options: { + // Any options you would normally pass to credential.getToken() + }, + tokenManagerConfig: { + expirationRefreshRatio: 0.8 + } +}); + +const client = createClient({ + url: 'redis://your-host', + credentialsProvider: provider +}); + +await client.connect(); +``` + +#### Important Notes on Using DefaultAzureCredential + +When using the `createForDefaultAzureCredential` method, you need to: + +1. Create your own instance of `DefaultAzureCredential` +2. Pass the same parameters to the factory method that you would use with the `getToken()` method: + - `scopes`: The Redis scope (use the exported `REDIS_SCOPE_DEFAULT` constant) + - `options`: Any additional options for the getToken method + +This factory method creates a wrapper around DefaultAzureCredential that adapts it to the Redis client's +authentication system, while maintaining all the flexibility of the original Azure Identity authentication. + ## Important Limitations ### RESP2 PUB/SUB Limitations diff --git a/packages/entraid/integration-tests/entraid-integration.spec.ts b/packages/entraid/integration-tests/entraid-integration.spec.ts index deb1d47dec1..4d078a01ede 100644 --- a/packages/entraid/integration-tests/entraid-integration.spec.ts +++ b/packages/entraid/integration-tests/entraid-integration.spec.ts @@ -1,6 +1,7 @@ +import { DefaultAzureCredential, EnvironmentCredential } from '@azure/identity'; import { BasicAuth } from '@redis/client/dist/lib/authx'; import { createClient } from '@redis/client'; -import { EntraIdCredentialsProviderFactory } from '../lib/entra-id-credentials-provider-factory'; +import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from '../lib/entra-id-credentials-provider-factory'; import { strict as assert } from 'node:assert'; import { spy, SinonSpy } from 'sinon'; import { randomUUID } from 'crypto'; @@ -51,6 +52,35 @@ describe('EntraID Integration Tests', () => { ); }); + it('client with DefaultAzureCredential should be able to authenticate/re-authenticate', async () => { + + const azureCredential = new DefaultAzureCredential(); + + await runAuthenticationTest(() => + EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({ + credential: azureCredential, + scopes: REDIS_SCOPE_DEFAULT, + tokenManagerConfig: { + expirationRefreshRatio: 0.00001 + } + }) + , { testingDefaultAzureCredential: true }); + }); + + it('client with EnvironmentCredential should be able to authenticate/re-authenticate', async () => { + const envCredential = new EnvironmentCredential(); + + await runAuthenticationTest(() => + EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({ + credential: envCredential, + scopes: REDIS_SCOPE_DEFAULT, + tokenManagerConfig: { + expirationRefreshRatio: 0.00001 + } + }) + , { testingDefaultAzureCredential: true }); + }); + interface TestConfig { clientId: string; clientSecret: string; @@ -83,15 +113,15 @@ describe('EntraID Integration Tests', () => { }); return { - endpoints: await loadFromFile(requiredEnvVars.REDIS_ENDPOINTS_CONFIG_PATH), - clientId: requiredEnvVars.AZURE_CLIENT_ID, - clientSecret: requiredEnvVars.AZURE_CLIENT_SECRET, - authority: requiredEnvVars.AZURE_AUTHORITY, - tenantId: requiredEnvVars.AZURE_TENANT_ID, - redisScopes: requiredEnvVars.AZURE_REDIS_SCOPES, - cert: requiredEnvVars.AZURE_CERT, - privateKey: requiredEnvVars.AZURE_PRIVATE_KEY, - userAssignedManagedId: requiredEnvVars.AZURE_USER_ASSIGNED_MANAGED_ID + endpoints: await loadFromFile(requiredEnvVars.REDIS_ENDPOINTS_CONFIG_PATH as string), + clientId: requiredEnvVars.AZURE_CLIENT_ID as string, + clientSecret: requiredEnvVars.AZURE_CLIENT_SECRET as string, + authority: requiredEnvVars.AZURE_AUTHORITY as string, + tenantId: requiredEnvVars.AZURE_TENANT_ID as string, + redisScopes: requiredEnvVars.AZURE_REDIS_SCOPES as string, + cert: requiredEnvVars.AZURE_CERT as string, + privateKey: requiredEnvVars.AZURE_PRIVATE_KEY as string, + userAssignedManagedId: requiredEnvVars.AZURE_USER_ASSIGNED_MANAGED_ID as string }; }; @@ -127,12 +157,22 @@ describe('EntraID Integration Tests', () => { } }; - const validateTokens = (reAuthSpy: SinonSpy) => { + /** + * Validates authentication tokens generated during re-authentication + * + * @param reAuthSpy - The Sinon spy on the reAuthenticate method + * @param skipUniqueCheckForDefaultAzureCredential - Skip the unique check for DefaultAzureCredential as there are no guarantees that the tokens will be unique + * if the test is using default azure credential + */ + const validateTokens = (reAuthSpy: SinonSpy, skipUniqueCheckForDefaultAzureCredential: boolean) => { assert(reAuthSpy.callCount >= 1, `reAuthenticate should have been called at least once, but was called ${reAuthSpy.callCount} times`); const tokenDetails: TokenDetail[] = reAuthSpy.getCalls().map(call => { const creds = call.args[0] as BasicAuth; + if (!creds.password) { + throw new Error('Expected password to be set in BasicAuth credentials'); + } const tokenPayload = JSON.parse( Buffer.from(creds.password.split('.')[1], 'base64').toString() ); @@ -146,38 +186,43 @@ describe('EntraID Integration Tests', () => { }; }); - // Verify unique tokens - const uniqueTokens = new Set(tokenDetails.map(detail => detail.token)); - assert.equal( - uniqueTokens.size, - reAuthSpy.callCount, - `Expected ${reAuthSpy.callCount} different tokens, but got ${uniqueTokens.size} unique tokens` - ); + // we can't guarantee that the tokens will be unique when using DefaultAzureCredential + if (!skipUniqueCheckForDefaultAzureCredential) { + // Verify unique tokens + const uniqueTokens = new Set(tokenDetails.map(detail => detail.token)); + assert.equal( + uniqueTokens.size, + reAuthSpy.callCount, + `Expected ${reAuthSpy.callCount} different tokens, but got ${uniqueTokens.size} unique tokens` + ); - // Verify all tokens are not cached (i.e. have the same lifetime) - const uniqueLifetimes = new Set(tokenDetails.map(detail => detail.lifetime)); - assert.equal( - uniqueLifetimes.size, - 1, - `Expected all tokens to have the same lifetime, but found ${uniqueLifetimes.size} different lifetimes: ${[uniqueLifetimes].join(', ')} seconds` - ); + // Verify all tokens are not cached (i.e. have the same lifetime) + const uniqueLifetimes = new Set(tokenDetails.map(detail => detail.lifetime)); + assert.equal( + uniqueLifetimes.size, + 1, + `Expected all tokens to have the same lifetime, but found ${uniqueLifetimes.size} different lifetimes: ${(Array.from(uniqueLifetimes).join(','))} seconds` + ); - // Verify that all tokens have different uti (unique token identifier) - const uniqueUti = new Set(tokenDetails.map(detail => detail.uti)); - assert.equal( - uniqueUti.size, - reAuthSpy.callCount, - `Expected all tokens to have different uti, but found ${uniqueUti.size} different uti in: ${[uniqueUti].join(', ')}` - ); + // Verify that all tokens have different uti (unique token identifier) + const uniqueUti = new Set(tokenDetails.map(detail => detail.uti)); + assert.equal( + uniqueUti.size, + reAuthSpy.callCount, + `Expected all tokens to have different uti, but found ${uniqueUti.size} different uti in: ${(Array.from(uniqueUti).join(','))}` + ); + } }; - const runAuthenticationTest = async (setupCredentialsProvider: () => any) => { + const runAuthenticationTest = async (setupCredentialsProvider: () => any, options: { + testingDefaultAzureCredential: boolean + } = { testingDefaultAzureCredential: false }) => { const { client, reAuthSpy } = await setupTestClient(setupCredentialsProvider()); try { await client.connect(); await runClientOperations(client); - validateTokens(reAuthSpy); + validateTokens(reAuthSpy, options.testingDefaultAzureCredential); } finally { await client.destroy(); } diff --git a/packages/entraid/lib/azure-identity-provider.ts b/packages/entraid/lib/azure-identity-provider.ts new file mode 100644 index 00000000000..d522c9d4b89 --- /dev/null +++ b/packages/entraid/lib/azure-identity-provider.ts @@ -0,0 +1,22 @@ +import type { AccessToken } from '@azure/core-auth'; + +import { IdentityProvider, TokenResponse } from '@redis/client/dist/lib/authx'; + +export class AzureIdentityProvider implements IdentityProvider { + private readonly getToken: () => Promise; + + constructor(getToken: () => Promise) { + this.getToken = getToken; + } + + async requestToken(): Promise> { + const result = await this.getToken(); + return { + token: result, + ttlMs: result.expiresOnTimestamp - Date.now() + }; + } + +} + + diff --git a/packages/entraid/lib/entra-id-credentials-provider-factory.ts b/packages/entraid/lib/entra-id-credentials-provider-factory.ts index 0f89be8039b..98a3a11078a 100644 --- a/packages/entraid/lib/entra-id-credentials-provider-factory.ts +++ b/packages/entraid/lib/entra-id-credentials-provider-factory.ts @@ -1,3 +1,4 @@ +import type { GetTokenOptions, TokenCredential } from '@azure/core-auth'; import { NetworkError } from '@azure/msal-common'; import { LogLevel, @@ -7,8 +8,9 @@ import { PublicClientApplication, ConfidentialClientApplication, AuthorizationUrlRequest, AuthorizationCodeRequest, CryptoProvider, Configuration, NodeAuthOptions, AccountInfo } from '@azure/msal-node'; -import { RetryPolicy, TokenManager, TokenManagerConfig, ReAuthenticationError } from '@redis/client/dist/lib/authx'; -import { EntraidCredentialsProvider } from './entraid-credentials-provider'; +import { RetryPolicy, TokenManager, TokenManagerConfig, ReAuthenticationError, BasicAuth } from '@redis/client/dist/lib/authx'; +import { AzureIdentityProvider } from './azure-identity-provider'; +import { AuthenticationResponse, DEFAULT_CREDENTIALS_MAPPER, EntraidCredentialsProvider, OID_CREDENTIALS_MAPPER } from './entraid-credentials-provider'; import { MSALIdentityProvider } from './msal-identity-provider'; /** @@ -51,7 +53,11 @@ export class EntraIdCredentialsProviderFactory { return new EntraidCredentialsProvider( new TokenManager(idp, params.tokenManagerConfig), idp, - { onReAuthenticationError: params.onReAuthenticationError, credentialsMapper: OID_CREDENTIALS_MAPPER } + { + onReAuthenticationError: params.onReAuthenticationError, + credentialsMapper: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER, + onRetryableError: params.onRetryableError + } ); } @@ -102,7 +108,8 @@ export class EntraIdCredentialsProviderFactory { return new EntraidCredentialsProvider(new TokenManager(idp, params.tokenManagerConfig), idp, { onReAuthenticationError: params.onReAuthenticationError, - credentialsMapper: OID_CREDENTIALS_MAPPER + credentialsMapper: params.credentialsMapper ?? OID_CREDENTIALS_MAPPER, + onRetryableError: params.onRetryableError }); } @@ -138,6 +145,42 @@ export class EntraIdCredentialsProviderFactory { ); } + /** + * This method is used to create a credentials provider using DefaultAzureCredential. + * + * The user needs to create a configured instance of DefaultAzureCredential ( or any other class that implements TokenCredential )and pass it to this method. + * + * The default credentials mapper for this method is OID_CREDENTIALS_MAPPER which extracts the object ID from JWT + * encoded token. + * + * Depending on the actual flow that DefaultAzureCredential uses, the user may need to provide different + * credential mapper via the credentialsMapper parameter. + * + */ + static createForDefaultAzureCredential( + { + credential, + scopes, + options, + tokenManagerConfig, + onReAuthenticationError, + credentialsMapper, + onRetryableError + }: DefaultAzureCredentialsParams + ): EntraidCredentialsProvider { + + const idp = new AzureIdentityProvider( + () => credential.getToken(scopes, options).then(x => x === null ? Promise.reject('Token is null') : x) + ); + + return new EntraidCredentialsProvider(new TokenManager(idp, tokenManagerConfig), idp, + { + onReAuthenticationError: onReAuthenticationError, + credentialsMapper: credentialsMapper ?? OID_CREDENTIALS_MAPPER, + onRetryableError: onRetryableError + }); + } + /** * This method is used to create a credentials provider for the Authorization Code Flow with PKCE. * @param params @@ -194,7 +237,11 @@ export class EntraIdCredentialsProviderFactory { } ); const tm = new TokenManager(idp, params.tokenManagerConfig); - return new EntraidCredentialsProvider(tm, idp, { onReAuthenticationError: params.onReAuthenticationError }); + return new EntraidCredentialsProvider(tm, idp, { + onReAuthenticationError: params.onReAuthenticationError, + credentialsMapper: params.credentialsMapper ?? DEFAULT_CREDENTIALS_MAPPER, + onRetryableError: params.onRetryableError + }); } }; } @@ -214,8 +261,8 @@ export class EntraIdCredentialsProviderFactory { } -const REDIS_SCOPE_DEFAULT = 'https://redis.azure.com/.default'; -const REDIS_SCOPE = 'https://redis.azure.com' +export const REDIS_SCOPE_DEFAULT = 'https://redis.azure.com/.default'; +export const REDIS_SCOPE = 'https://redis.azure.com' export type AuthorityConfig = | { type: 'multi-tenant'; tenantId: string } @@ -234,7 +281,19 @@ export type CredentialParams = { authorityConfig?: AuthorityConfig; tokenManagerConfig: TokenManagerConfig - onReAuthenticationError?: (error: ReAuthenticationError) => void; + onReAuthenticationError?: (error: ReAuthenticationError) => void + credentialsMapper?: (token: AuthenticationResponse) => BasicAuth + onRetryableError?: (error: string) => void +} + +export type DefaultAzureCredentialsParams = { + scopes: string | string[], + options?: GetTokenOptions, + credential: TokenCredential + tokenManagerConfig: TokenManagerConfig + onReAuthenticationError?: (error: ReAuthenticationError) => void + credentialsMapper?: (token: AuthenticationResponse) => BasicAuth + onRetryableError?: (error: string) => void } export type AuthCodePKCEParams = CredentialParams & { @@ -356,16 +415,3 @@ export class AuthCodeFlowHelper { } } -const OID_CREDENTIALS_MAPPER = (token: AuthenticationResult) => { - - // Client credentials flow is app-only authentication (no user context), - // so only access token is provided without user-specific claims (uniqueId, idToken, ...) - // this means that we need to extract the oid from the access token manually - const accessToken = JSON.parse(Buffer.from(token.accessToken.split('.')[1], 'base64').toString()); - - return ({ - username: accessToken.oid, - password: token.accessToken - }) - -} diff --git a/packages/entraid/lib/entraid-credentials-provider.ts b/packages/entraid/lib/entraid-credentials-provider.ts index 115d6dbff3a..465c9e8a975 100644 --- a/packages/entraid/lib/entraid-credentials-provider.ts +++ b/packages/entraid/lib/entraid-credentials-provider.ts @@ -1,4 +1,5 @@ import { AuthenticationResult } from '@azure/msal-common/node'; +import { AccessToken } from '@azure/core-auth'; import { BasicAuth, StreamingCredentialsProvider, IdentityProvider, TokenManager, ReAuthenticationError, StreamingCredentialsListener, IDPError, Token, Disposable @@ -9,6 +10,9 @@ import { * Please use one of the factory functions in `entraid-credetfactories.ts` to create an instance of this class for the different * type of authentication flows. */ + +export type AuthenticationResponse = AuthenticationResult | AccessToken + export class EntraidCredentialsProvider implements StreamingCredentialsProvider { readonly type = 'streaming-credentials-provider'; @@ -24,11 +28,11 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider }> = []; constructor( - public readonly tokenManager: TokenManager, - public readonly idp: IdentityProvider, + public readonly tokenManager: TokenManager, + public readonly idp: IdentityProvider, private readonly options: { onReAuthenticationError?: (error: ReAuthenticationError) => void; - credentialsMapper?: (token: AuthenticationResult) => BasicAuth; + credentialsMapper?: (token: AuthenticationResponse) => BasicAuth; onRetryableError?: (error: string) => void; } = {} ) { @@ -69,7 +73,7 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider onReAuthenticationError: (error: ReAuthenticationError) => void; - #credentialsMapper: (token: AuthenticationResult) => BasicAuth; + #credentialsMapper: (token: AuthenticationResponse) => BasicAuth; #createTokenManagerListener(subscribers: Set>) { return { @@ -80,7 +84,7 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider this.options.onRetryableError?.(error.message); } }, - onNext: (token: { value: AuthenticationResult }): void => { + onNext: (token: { value: AuthenticationResult | AccessToken }): void => { const credentials = this.#credentialsMapper(token.value); subscribers.forEach(listener => listener.onNext(credentials)); } @@ -101,10 +105,10 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider }; } - async #startTokenManagerAndObtainInitialToken(): Promise> { - const initialResponse = await this.idp.requestToken(); - const token = this.tokenManager.wrapAndSetCurrentToken(initialResponse.token, initialResponse.ttlMs); + async #startTokenManagerAndObtainInitialToken(): Promise> { + const { ttlMs, token: initialToken } = await this.idp.requestToken(); + const token = this.tokenManager.wrapAndSetCurrentToken(initialToken, ttlMs); this.#tokenManagerDisposable = this.tokenManager.start( this.#createTokenManagerListener(this.#listeners), this.tokenManager.calculateRefreshTime(token) @@ -131,10 +135,61 @@ export class EntraidCredentialsProvider implements StreamingCredentialsProvider } -const DEFAULT_CREDENTIALS_MAPPER = (token: AuthenticationResult): BasicAuth => ({ - username: token.uniqueId, - password: token.accessToken -}); +export const DEFAULT_CREDENTIALS_MAPPER = (token: AuthenticationResponse): BasicAuth => { + if (isAuthenticationResult(token)) { + return { + username: token.uniqueId, + password: token.accessToken + } + } else { + return OID_CREDENTIALS_MAPPER(token) + } +}; const DEFAULT_ERROR_HANDLER = (error: ReAuthenticationError) => - console.error('ReAuthenticationError', error); \ No newline at end of file + console.error('ReAuthenticationError', error); + +export const OID_CREDENTIALS_MAPPER = (token: (AuthenticationResult | AccessToken)) => { + + if (isAuthenticationResult(token)) { + // Client credentials flow is app-only authentication (no user context), + // so only access token is provided without user-specific claims (uniqueId, idToken, ...) + // this means that we need to extract the oid from the access token manually + const accessToken = JSON.parse(Buffer.from(token.accessToken.split('.')[1], 'base64').toString()); + + return ({ + username: accessToken.oid, + password: token.accessToken + }) + } else { + const accessToken = JSON.parse(Buffer.from(token.token.split('.')[1], 'base64').toString()); + + return ({ + username: accessToken.oid, + password: token.token + }) + } + +} + +/** + * Type guard to check if a token is an MSAL AuthenticationResult + * + * @param auth - The token to check + * @returns true if the token is an AuthenticationResult + */ +export function isAuthenticationResult(auth: AuthenticationResult | AccessToken): auth is AuthenticationResult { + return typeof (auth as AuthenticationResult).accessToken === 'string' && + !('token' in auth) +} + +/** + * Type guard to check if a token is an Azure Identity AccessToken + * + * @param auth - The token to check + * @returns true if the token is an AccessToken + */ +export function isAccessToken(auth: AuthenticationResult | AccessToken): auth is AccessToken { + return typeof (auth as AccessToken).token === 'string' && + !('accessToken' in auth); +} \ No newline at end of file diff --git a/packages/entraid/lib/msal-identity-provider.ts b/packages/entraid/lib/msal-identity-provider.ts index 59b38d18ec6..0f15e01fcdc 100644 --- a/packages/entraid/lib/msal-identity-provider.ts +++ b/packages/entraid/lib/msal-identity-provider.ts @@ -11,21 +11,15 @@ export class MSALIdentityProvider implements IdentityProvider> { - try { - const result = await this.getToken(); + const result = await this.getToken(); - if (!result?.accessToken || !result?.expiresOn) { - throw new Error('Invalid token response'); - } - return { - token: result, - ttlMs: result.expiresOn.getTime() - Date.now() - }; - } catch (error) { - throw error; + if (!result?.accessToken || !result?.expiresOn) { + throw new Error('Invalid token response'); } + return { + token: result, + ttlMs: result.expiresOn.getTime() - Date.now() + }; } } - - diff --git a/packages/entraid/package.json b/packages/entraid/package.json index da0d7df9935..fd504cd44dc 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -12,10 +12,12 @@ "clean": "rimraf dist", "build": "npm run clean && tsc", "start:auth-pkce": "tsx --tsconfig tsconfig.samples.json ./samples/auth-code-pkce/index.ts", + "start:interactive-browser": "tsx --tsconfig tsconfig.samples.json ./samples/interactive-browser/index.ts", "test-integration": "mocha -r tsx --tsconfig tsconfig.integration-tests.json './integration-tests/**/*.spec.ts'", "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "dependencies": { + "@azure/identity": "4.7.0", "@azure/msal-node": "^2.16.1" }, "peerDependencies": { diff --git a/packages/entraid/samples/interactive-browser/index.ts b/packages/entraid/samples/interactive-browser/index.ts new file mode 100644 index 00000000000..f458ad9e190 --- /dev/null +++ b/packages/entraid/samples/interactive-browser/index.ts @@ -0,0 +1,111 @@ +import express, { Request, Response } from 'express'; +import session from 'express-session'; +import dotenv from 'dotenv'; +import { DEFAULT_TOKEN_MANAGER_CONFIG, EntraIdCredentialsProviderFactory } from '../../lib/entra-id-credentials-provider-factory'; +import { InteractiveBrowserCredential } from '@azure/identity'; + +dotenv.config(); + +if (!process.env.SESSION_SECRET) { + throw new Error('SESSION_SECRET environment variable must be set'); +} + +const app = express(); + +const sessionConfig = { + secret: process.env.SESSION_SECRET, + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV === 'production', // Only use secure in production + httpOnly: true, + sameSite: 'lax', + maxAge: 3600000 // 1 hour + } +} as const; + +app.use(session(sessionConfig)); + +if (!process.env.MSAL_CLIENT_ID || !process.env.MSAL_TENANT_ID) { + throw new Error('MSAL_CLIENT_ID and MSAL_TENANT_ID environment variables must be set'); +} + + +app.get('/login', async (req: Request, res: Response) => { + try { + // Create an instance of InteractiveBrowserCredential + const credential = new InteractiveBrowserCredential({ + clientId: process.env.MSAL_CLIENT_ID!, + tenantId: process.env.MSAL_TENANT_ID!, + loginStyle: 'popup', + redirectUri: 'http://localhost:3000/redirect' + }); + + // Create Redis client using the EntraID credentials provider + const entraidCredentialsProvider = EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({ + credential, + scopes: ['user.read'], + tokenManagerConfig: DEFAULT_TOKEN_MANAGER_CONFIG + }); + + // Subscribe to credentials updates + const initialCredentials = entraidCredentialsProvider.subscribe({ + onNext: (token) => { + // Never log the full token in production + console.log('Token acquired successfully'); + console.log('Username:', token.username); + + }, + onError: (error) => { + console.error('Token acquisition failed:', error); + } + }); + + // Wait for the initial credentials + const [credentials] = await initialCredentials; + + // Return success response + res.json({ + status: 'success', + message: 'Authentication successful', + credentials: { + username: credentials.username, + password: credentials.password + } + }); + } catch (error) { + console.error('Authentication failed:', error); + res.status(500).json({ + status: 'error', + message: 'Authentication failed', + error: error instanceof Error ? error.message : String(error) + }); + } +}); + +// Create a simple status page +app.get('/', (req: Request, res: Response) => { + res.send(` + + + Interactive Browser Credential Demo + + + +

Interactive Browser Credential Demo

+

This example demonstrates using the InteractiveBrowserCredential from @azure/identity to authenticate with Microsoft Entra ID.

+

When you click the button below, you'll be redirected to the Microsoft login page.

+ Login with Microsoft + + + `); +}); + +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`Server running on port ${PORT}`); + console.log(`Open http://localhost:${PORT} in your browser to start`); +}); From ca85f8268ded7909b03300b156b43cf91bbf7182 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Tue, 18 Mar 2025 14:27:37 +0200 Subject: [PATCH 044/105] refactor!: Remove graph module (#2897) https://redis.io/blog/redisgraph-eol/ --- .github/release-drafter/graph-config.yml | 49 --- .github/workflows/release-drafter-graph.yml | 24 -- README.md | 1 - docs/v4-to-v5.md | 4 - packages/graph/.nycrc.json | 4 - packages/graph/.release-it.json | 11 - packages/graph/README.md | 34 -- .../graph/lib/commands/CONFIG_GET.spec.ts | 23 -- packages/graph/lib/commands/CONFIG_GET.ts | 16 - .../graph/lib/commands/CONFIG_SET.spec.ts | 20 - packages/graph/lib/commands/CONFIG_SET.ts | 11 - packages/graph/lib/commands/DELETE.spec.ts | 22 -- packages/graph/lib/commands/DELETE.ts | 11 - packages/graph/lib/commands/EXPLAIN.spec.ts | 24 -- packages/graph/lib/commands/EXPLAIN.ts | 12 - packages/graph/lib/commands/LIST.spec.ts | 20 - packages/graph/lib/commands/LIST.ts | 11 - packages/graph/lib/commands/PROFILE.spec.ts | 21 - packages/graph/lib/commands/PROFILE.ts | 12 - packages/graph/lib/commands/QUERY.spec.ts | 64 ---- packages/graph/lib/commands/QUERY.ts | 100 ----- packages/graph/lib/commands/RO_QUERY.spec.ts | 21 - packages/graph/lib/commands/RO_QUERY.ts | 8 - packages/graph/lib/commands/SLOWLOG.spec.ts | 21 - packages/graph/lib/commands/SLOWLOG.ts | 28 -- packages/graph/lib/commands/index.ts | 31 -- packages/graph/lib/graph.spec.ts | 148 -------- packages/graph/lib/graph.ts | 359 ------------------ packages/graph/lib/index.ts | 2 - packages/graph/lib/test-utils.ts | 22 -- packages/graph/package.json | 35 -- packages/graph/tsconfig.json | 20 - packages/redis/index.ts | 3 - packages/redis/package.json | 1 - tsconfig.json | 3 - 35 files changed, 1196 deletions(-) delete mode 100644 .github/release-drafter/graph-config.yml delete mode 100644 .github/workflows/release-drafter-graph.yml delete mode 100644 packages/graph/.nycrc.json delete mode 100644 packages/graph/.release-it.json delete mode 100644 packages/graph/README.md delete mode 100644 packages/graph/lib/commands/CONFIG_GET.spec.ts delete mode 100644 packages/graph/lib/commands/CONFIG_GET.ts delete mode 100644 packages/graph/lib/commands/CONFIG_SET.spec.ts delete mode 100644 packages/graph/lib/commands/CONFIG_SET.ts delete mode 100644 packages/graph/lib/commands/DELETE.spec.ts delete mode 100644 packages/graph/lib/commands/DELETE.ts delete mode 100644 packages/graph/lib/commands/EXPLAIN.spec.ts delete mode 100644 packages/graph/lib/commands/EXPLAIN.ts delete mode 100644 packages/graph/lib/commands/LIST.spec.ts delete mode 100644 packages/graph/lib/commands/LIST.ts delete mode 100644 packages/graph/lib/commands/PROFILE.spec.ts delete mode 100644 packages/graph/lib/commands/PROFILE.ts delete mode 100644 packages/graph/lib/commands/QUERY.spec.ts delete mode 100644 packages/graph/lib/commands/QUERY.ts delete mode 100644 packages/graph/lib/commands/RO_QUERY.spec.ts delete mode 100644 packages/graph/lib/commands/RO_QUERY.ts delete mode 100644 packages/graph/lib/commands/SLOWLOG.spec.ts delete mode 100644 packages/graph/lib/commands/SLOWLOG.ts delete mode 100644 packages/graph/lib/commands/index.ts delete mode 100644 packages/graph/lib/graph.spec.ts delete mode 100644 packages/graph/lib/graph.ts delete mode 100644 packages/graph/lib/index.ts delete mode 100644 packages/graph/lib/test-utils.ts delete mode 100644 packages/graph/package.json delete mode 100644 packages/graph/tsconfig.json diff --git a/.github/release-drafter/graph-config.yml b/.github/release-drafter/graph-config.yml deleted file mode 100644 index 88d76b78b55..00000000000 --- a/.github/release-drafter/graph-config.yml +++ /dev/null @@ -1,49 +0,0 @@ -name-template: 'graph@$NEXT_PATCH_VERSION' -tag-template: 'graph@$NEXT_PATCH_VERSION' -autolabeler: - - label: 'chore' - files: - - '*.md' - - '.github/*' - - label: 'bug' - branch: - - '/bug-.+' - - label: 'chore' - branch: - - '/chore-.+' - - label: 'feature' - branch: - - '/feature-.+' -categories: - - title: 'Breaking Changes' - labels: - - 'breakingchange' - - title: '🚀 New Features' - labels: - - 'feature' - - 'enhancement' - - title: '🐛 Bug Fixes' - labels: - - 'fix' - - 'bugfix' - - 'bug' - - title: '🧰 Maintenance' - label: - - 'chore' - - 'maintenance' - - 'documentation' - - 'docs' -change-template: '- $TITLE (#$NUMBER)' -include-paths: - - 'packages/graph' -exclude-labels: - - 'skip-changelog' -template: | - ## Changes - - $CHANGES - - ## Contributors - We'd like to thank all the contributors who worked on this release! - - $CONTRIBUTORS diff --git a/.github/workflows/release-drafter-graph.yml b/.github/workflows/release-drafter-graph.yml deleted file mode 100644 index 4d664e5f19e..00000000000 --- a/.github/workflows/release-drafter-graph.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Release Drafter - -on: - push: - # branches to consider in the event; optional, defaults to all - branches: - - master - -jobs: - - update_release_draft: - - permissions: - contents: write - pull-requests: write - runs-on: ubuntu-latest - steps: - # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: release-drafter/release-drafter@v5 - with: - # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml - config-name: release-drafter/graph-config.yml - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 457f3d2c2cf..8e7e3845256 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,6 @@ npm install redis | [`redis`](./packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | | [`@redis/client`](./packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | | [`@redis/bloom`](./packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | -| [`@redis/graph`](./packages/graph) | [Redis Graph](https://redis.io/docs/data-types/probabilistic/) commands | | [`@redis/json`](./packages/json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | | [`@redis/search`](./packages/search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | | [`@redis/time-series`](./packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | diff --git a/docs/v4-to-v5.md b/docs/v4-to-v5.md index 95c2230ce23..57ad3f9bbf6 100644 --- a/docs/v4-to-v5.md +++ b/docs/v4-to-v5.md @@ -211,10 +211,6 @@ In v5, any unwritten commands (in the same pipeline) will be discarded. - `TOPK.QUERY`: `Array` -> `Array` -### Graph - -- `GRAPH.SLOWLOG`: `timestamp` has been changed from `Date` to `number` - ### JSON - `JSON.ARRINDEX`: `start` and `end` arguments moved to `{ range: { start: number; end: number; }; }` [^future-proofing] diff --git a/packages/graph/.nycrc.json b/packages/graph/.nycrc.json deleted file mode 100644 index 367a89ad32c..00000000000 --- a/packages/graph/.nycrc.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "@istanbuljs/nyc-config-typescript", - "exclude": ["dist", "**/*.spec.ts", "lib/test-utils.ts"] -} diff --git a/packages/graph/.release-it.json b/packages/graph/.release-it.json deleted file mode 100644 index 7797dd0b4dd..00000000000 --- a/packages/graph/.release-it.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "git": { - "tagName": "graph@${version}", - "commitMessage": "Release ${tagName}", - "tagAnnotation": "Release ${tagName}" - }, - "npm": { - "versionArgs": ["--workspaces-update=false"], - "publishArgs": ["--access", "public"] - } -} diff --git a/packages/graph/README.md b/packages/graph/README.md deleted file mode 100644 index 4c712bfd820..00000000000 --- a/packages/graph/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# @redis/graph - -Example usage: -```javascript -import { createClient, Graph } from 'redis'; - -const client = createClient(); -client.on('error', (err) => console.log('Redis Client Error', err)); - -await client.connect(); - -const graph = new Graph(client, 'graph'); - -await graph.query( - 'CREATE (:Rider { name: $riderName })-[:rides]->(:Team { name: $teamName })', - { - params: { - riderName: 'Buzz Aldrin', - teamName: 'Apollo' - } - } -); - -const result = await graph.roQuery( - 'MATCH (r:Rider)-[:rides]->(t:Team { name: $name }) RETURN r.name AS name', - { - params: { - name: 'Apollo' - } - } -); - -console.log(result.data); // [{ name: 'Buzz Aldrin' }] -``` diff --git a/packages/graph/lib/commands/CONFIG_GET.spec.ts b/packages/graph/lib/commands/CONFIG_GET.spec.ts deleted file mode 100644 index 9a427867c63..00000000000 --- a/packages/graph/lib/commands/CONFIG_GET.spec.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import CONFIG_GET from './CONFIG_GET'; -import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - -describe('GRAPH.CONFIG GET', () => { - it('transformArguments', () => { - assert.deepEqual( - parseArgs(CONFIG_GET, 'TIMEOUT'), - ['GRAPH.CONFIG', 'GET', 'TIMEOUT'] - ); - }); - - testUtils.testWithClient('client.graph.configGet', async client => { - assert.deepEqual( - await client.graph.configGet('TIMEOUT'), - [ - 'TIMEOUT', - 0 - ] - ); - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/graph/lib/commands/CONFIG_GET.ts b/packages/graph/lib/commands/CONFIG_GET.ts deleted file mode 100644 index 4986dfc1816..00000000000 --- a/packages/graph/lib/commands/CONFIG_GET.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { RedisArgument, TuplesReply, ArrayReply, BlobStringReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { CommandParser } from '@redis/client/dist/lib/client/parser'; - -type ConfigItemReply = TuplesReply<[ - configKey: BlobStringReply, - value: NumberReply -]>; - -export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, - parseCommand(parser: CommandParser, configKey: RedisArgument) { - parser.push('GRAPH.CONFIG', 'GET', configKey); - }, - transformReply: undefined as unknown as () => ConfigItemReply | ArrayReply -} as const satisfies Command; diff --git a/packages/graph/lib/commands/CONFIG_SET.spec.ts b/packages/graph/lib/commands/CONFIG_SET.spec.ts deleted file mode 100644 index ae6e296699c..00000000000 --- a/packages/graph/lib/commands/CONFIG_SET.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import CONFIG_SET from './CONFIG_SET'; -import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - -describe('GRAPH.CONFIG SET', () => { - it('transformArguments', () => { - assert.deepEqual( - parseArgs(CONFIG_SET, 'TIMEOUT', 0), - ['GRAPH.CONFIG', 'SET', 'TIMEOUT', '0'] - ); - }); - - testUtils.testWithClient('client.graph.configSet', async client => { - assert.equal( - await client.graph.configSet('TIMEOUT', 0), - 'OK' - ); - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/graph/lib/commands/CONFIG_SET.ts b/packages/graph/lib/commands/CONFIG_SET.ts deleted file mode 100644 index 63f604402ec..00000000000 --- a/packages/graph/lib/commands/CONFIG_SET.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { CommandParser } from '@redis/client/dist/lib/client/parser'; - -export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: false, - parseCommand(parser: CommandParser, configKey: RedisArgument, value: number) { - parser.push('GRAPH.CONFIG', 'SET', configKey, value.toString()); - }, - transformReply: undefined as unknown as () => SimpleStringReply<'OK'> -} as const satisfies Command; diff --git a/packages/graph/lib/commands/DELETE.spec.ts b/packages/graph/lib/commands/DELETE.spec.ts deleted file mode 100644 index 5977c646307..00000000000 --- a/packages/graph/lib/commands/DELETE.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import DELETE from './DELETE'; -import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - -describe('GRAPH.DELETE', () => { - it('transformArguments', () => { - assert.deepEqual( - parseArgs(DELETE, 'key'), - ['GRAPH.DELETE', 'key'] - ); - }); - - testUtils.testWithClient('client.graph.delete', async client => { - const [, reply] = await Promise.all([ - client.graph.query('key', 'RETURN 1'), - client.graph.delete('key') - ]); - - assert.equal(typeof reply, 'string'); - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/graph/lib/commands/DELETE.ts b/packages/graph/lib/commands/DELETE.ts deleted file mode 100644 index 43af38d6fb0..00000000000 --- a/packages/graph/lib/commands/DELETE.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { RedisArgument, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { CommandParser } from '@redis/client/dist/lib/client/parser'; - -export default { - IS_READ_ONLY: false, - parseCommand(parser: CommandParser, key: RedisArgument) { - parser.push('GRAPH.DELETE'); - parser.pushKey(key); - }, - transformReply: undefined as unknown as () => BlobStringReply -} as const satisfies Command; diff --git a/packages/graph/lib/commands/EXPLAIN.spec.ts b/packages/graph/lib/commands/EXPLAIN.spec.ts deleted file mode 100644 index 28f30cd17b3..00000000000 --- a/packages/graph/lib/commands/EXPLAIN.spec.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import EXPLAIN from './EXPLAIN'; -import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - -describe('GRAPH.EXPLAIN', () => { - it('transformArguments', () => { - assert.deepEqual( - parseArgs(EXPLAIN, 'key', 'RETURN 0'), - ['GRAPH.EXPLAIN', 'key', 'RETURN 0'] - ); - }); - - testUtils.testWithClient('client.graph.explain', async client => { - const [, reply] = await Promise.all([ - client.graph.query('key', 'RETURN 0'), // make sure to create a graph first - client.graph.explain('key', 'RETURN 0') - ]); - assert.ok(Array.isArray(reply)); - for (const item of reply) { - assert.equal(typeof item, 'string'); - } - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/graph/lib/commands/EXPLAIN.ts b/packages/graph/lib/commands/EXPLAIN.ts deleted file mode 100644 index a9af7e73a20..00000000000 --- a/packages/graph/lib/commands/EXPLAIN.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { CommandParser } from '@redis/client/dist/lib/client/parser'; - -export default { - IS_READ_ONLY: true, - parseCommand(parser: CommandParser, key: RedisArgument, query: RedisArgument) { - parser.push('GRAPH.EXPLAIN'); - parser.pushKey(key); - parser.push(query); - }, - transformReply: undefined as unknown as () => ArrayReply -} as const satisfies Command; diff --git a/packages/graph/lib/commands/LIST.spec.ts b/packages/graph/lib/commands/LIST.spec.ts deleted file mode 100644 index 19f18a0e30b..00000000000 --- a/packages/graph/lib/commands/LIST.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import LIST from './LIST'; -import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - -describe('GRAPH.LIST', () => { - it('transformArguments', () => { - assert.deepEqual( - parseArgs(LIST), - ['GRAPH.LIST'] - ); - }); - - testUtils.testWithClient('client.graph.list', async client => { - assert.deepEqual( - await client.graph.list(), - [] - ); - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/graph/lib/commands/LIST.ts b/packages/graph/lib/commands/LIST.ts deleted file mode 100644 index 70fa8bda812..00000000000 --- a/packages/graph/lib/commands/LIST.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { CommandParser } from '@redis/client/dist/lib/client/parser'; - -export default { - NOT_KEYED_COMMAND: true, - IS_READ_ONLY: true, - parseCommand(parser: CommandParser) { - parser.push('GRAPH.LIST'); - }, - transformReply: undefined as unknown as () => ArrayReply -} as const satisfies Command; diff --git a/packages/graph/lib/commands/PROFILE.spec.ts b/packages/graph/lib/commands/PROFILE.spec.ts deleted file mode 100644 index 7f16fd3ba58..00000000000 --- a/packages/graph/lib/commands/PROFILE.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import PROFILE from './PROFILE'; -import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - -describe('GRAPH.PROFILE', () => { - it('transformArguments', () => { - assert.deepEqual( - parseArgs(PROFILE, 'key', 'RETURN 0'), - ['GRAPH.PROFILE', 'key', 'RETURN 0'] - ); - }); - - testUtils.testWithClient('client.graph.profile', async client => { - const reply = await client.graph.profile('key', 'RETURN 0'); - assert.ok(Array.isArray(reply)); - for (const item of reply) { - assert.equal(typeof item, 'string'); - } - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/graph/lib/commands/PROFILE.ts b/packages/graph/lib/commands/PROFILE.ts deleted file mode 100644 index c3229fb9a04..00000000000 --- a/packages/graph/lib/commands/PROFILE.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { CommandParser } from '@redis/client/dist/lib/client/parser'; - -export default { - IS_READ_ONLY: true, - parseCommand(parser: CommandParser, key: RedisArgument, query: RedisArgument) { - parser.push('GRAPH.PROFILE'); - parser.pushKey(key); - parser.push(query); - }, - transformReply: undefined as unknown as () => ArrayReply -} as const satisfies Command; diff --git a/packages/graph/lib/commands/QUERY.spec.ts b/packages/graph/lib/commands/QUERY.spec.ts deleted file mode 100644 index 28c2189645b..00000000000 --- a/packages/graph/lib/commands/QUERY.spec.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import QUERY from './QUERY'; -import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - -describe('GRAPH.QUERY', () => { - describe('transformArguments', () => { - it('simple', () => { - assert.deepEqual( - parseArgs(QUERY, 'key', 'query'), - ['GRAPH.QUERY', 'key', 'query'] - ); - }); - - describe('params', () => { - it('all types', () => { - assert.deepEqual( - parseArgs(QUERY, 'key', 'query', { - params: { - null: null, - string: '"\\', - number: 0, - boolean: false, - array: [0], - object: {a: 0} - } - }), - ['GRAPH.QUERY', 'key', 'CYPHER null=null string="\\"\\\\" number=0 boolean=false array=[0] object={a:0} query'] - ); - }); - - it('TypeError', () => { - assert.throws(() => { - parseArgs(QUERY, 'key', 'query', { - params: { - a: Symbol() - } - }) - }, TypeError); - }); - }); - - it('TIMEOUT', () => { - assert.deepEqual( - parseArgs(QUERY, 'key', 'query', { - TIMEOUT: 1 - }), - ['GRAPH.QUERY', 'key', 'query', 'TIMEOUT', '1'] - ); - }); - - it('compact', () => { - assert.deepEqual( - parseArgs(QUERY, 'key', 'query', undefined, true), - ['GRAPH.QUERY', 'key', 'query', '--compact'] - ); - }); - }); - - testUtils.testWithClient('client.graph.query', async client => { - const { data } = await client.graph.query('key', 'RETURN 0'); - assert.deepEqual(data, [[0]]); - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/graph/lib/commands/QUERY.ts b/packages/graph/lib/commands/QUERY.ts deleted file mode 100644 index 23ea24c5768..00000000000 --- a/packages/graph/lib/commands/QUERY.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { RedisArgument, ArrayReply, BlobStringReply, NumberReply, NullReply, TuplesReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { CommandParser } from '@redis/client/dist/lib/client/parser'; - -type Headers = ArrayReply; - -type Data = ArrayReply; - -type Metadata = ArrayReply; - -type QueryRawReply = TuplesReply<[ - headers: Headers, - data: Data, - metadata: Metadata -] | [ - metadata: Metadata -]>; - -type QueryParam = null | string | number | boolean | QueryParams | Array; - -type QueryParams = { - [key: string]: QueryParam; -}; - -export interface QueryOptions { - params?: QueryParams; - TIMEOUT?: number; -} - -export function parseQueryArguments( - command: RedisArgument, - parser: CommandParser, - graph: RedisArgument, - query: RedisArgument, - options?: QueryOptions, - compact?: boolean -) { - parser.push(command); - parser.pushKey(graph); - const param = options?.params ? - `CYPHER ${queryParamsToString(options.params)} ${query}` : - query; - parser.push(param); - - if (options?.TIMEOUT !== undefined) { - parser.push('TIMEOUT', options.TIMEOUT.toString()); - } - - if (compact) { - parser.push('--compact'); - } -} - -function queryParamsToString(params: QueryParams) { - return Object.entries(params) - .map(([key, value]) => `${key}=${queryParamToString(value)}`) - .join(' '); -} - -function queryParamToString(param: QueryParam): string { - if (param === null) { - return 'null'; - } - - switch (typeof param) { - case 'string': - return `"${param.replace(/["\\]/g, '\\$&')}"`; - - case 'number': - case 'boolean': - return param.toString(); - } - - if (Array.isArray(param)) { - return `[${param.map(queryParamToString).join(',')}]`; - } else if (typeof param === 'object') { - const body = []; - for (const [key, value] of Object.entries(param)) { - body.push(`${key}:${queryParamToString(value)}`); - } - return `{${body.join(',')}}`; - } else { - throw new TypeError(`Unexpected param type ${typeof param} ${param}`) - } -} - -export default { - IS_READ_ONLY: false, - parseCommand: parseQueryArguments.bind(undefined, 'GRAPH.QUERY'), - transformReply(reply: UnwrapReply) { - return reply.length === 1 ? { - headers: undefined, - data: undefined, - metadata: reply[0] - } : { - headers: reply[0], - data: reply[1], - metadata: reply[2] - }; - } -} as const satisfies Command; diff --git a/packages/graph/lib/commands/RO_QUERY.spec.ts b/packages/graph/lib/commands/RO_QUERY.spec.ts deleted file mode 100644 index fa9459cf642..00000000000 --- a/packages/graph/lib/commands/RO_QUERY.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import RO_QUERY from './RO_QUERY'; -import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - -describe('GRAPH.RO_QUERY', () => { - it('transformArguments', () => { - assert.deepEqual( - parseArgs(RO_QUERY, 'key', 'query'), - ['GRAPH.RO_QUERY', 'key', 'query'] - ); - }); - - testUtils.testWithClient('client.graph.roQuery', async client => { - const [, { data }] = await Promise.all([ - client.graph.query('key', 'RETURN 0'), // make sure to create a graph first - client.graph.roQuery('key', 'RETURN 0') - ]); - assert.deepEqual(data, [[0]]); - }, GLOBAL.SERVERS.OPEN); -}); \ No newline at end of file diff --git a/packages/graph/lib/commands/RO_QUERY.ts b/packages/graph/lib/commands/RO_QUERY.ts deleted file mode 100644 index f052877b99d..00000000000 --- a/packages/graph/lib/commands/RO_QUERY.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Command } from '@redis/client/dist/lib/RESP/types'; -import QUERY, { parseQueryArguments } from './QUERY'; - -export default { - IS_READ_ONLY: true, - parseCommand: parseQueryArguments.bind(undefined, 'GRAPH.RO_QUERY'), - transformReply: QUERY.transformReply -} as const satisfies Command; diff --git a/packages/graph/lib/commands/SLOWLOG.spec.ts b/packages/graph/lib/commands/SLOWLOG.spec.ts deleted file mode 100644 index b991d6e7b96..00000000000 --- a/packages/graph/lib/commands/SLOWLOG.spec.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from '../test-utils'; -import SLOWLOG from './SLOWLOG'; -import { parseArgs } from '@redis/client/lib/commands/generic-transformers'; - -describe('GRAPH.SLOWLOG', () => { - it('transformArguments', () => { - assert.deepEqual( - parseArgs(SLOWLOG, 'key'), - ['GRAPH.SLOWLOG', 'key'] - ); - }); - - testUtils.testWithClient('client.graph.slowLog', async client => { - const [, reply] = await Promise.all([ - client.graph.query('key', 'RETURN 1'), - client.graph.slowLog('key') - ]); - assert.equal(reply.length, 1); - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/graph/lib/commands/SLOWLOG.ts b/packages/graph/lib/commands/SLOWLOG.ts deleted file mode 100644 index 4110335f922..00000000000 --- a/packages/graph/lib/commands/SLOWLOG.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { RedisArgument, ArrayReply, TuplesReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types'; -import { CommandParser } from '@redis/client/dist/lib/client/parser'; - -type SlowLogRawReply = ArrayReply>; - -export default { - IS_READ_ONLY: true, - parseCommand(parser: CommandParser, key: RedisArgument) { - parser.push('GRAPH.SLOWLOG'); - parser.pushKey(key); - }, - transformReply(reply: UnwrapReply) { - return reply.map(log => { - const [timestamp, command, query, took] = log as unknown as UnwrapReply; - return { - timestamp: Number(timestamp), - command, - query, - took: Number(took) - }; - }); - } -} as const satisfies Command; diff --git a/packages/graph/lib/commands/index.ts b/packages/graph/lib/commands/index.ts deleted file mode 100644 index e93356aa951..00000000000 --- a/packages/graph/lib/commands/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { RedisCommands } from '@redis/client/dist/lib/RESP/types'; -import CONFIG_GET from './CONFIG_GET'; -import CONFIG_SET from './CONFIG_SET';; -import DELETE from './DELETE'; -import EXPLAIN from './EXPLAIN'; -import LIST from './LIST'; -import PROFILE from './PROFILE'; -import QUERY from './QUERY'; -import RO_QUERY from './RO_QUERY'; -import SLOWLOG from './SLOWLOG'; - -export default { - CONFIG_GET, - configGet: CONFIG_GET, - CONFIG_SET, - configSet: CONFIG_SET, - DELETE, - delete: DELETE, - EXPLAIN, - explain: EXPLAIN, - LIST, - list: LIST, - PROFILE, - profile: PROFILE, - QUERY, - query: QUERY, - RO_QUERY, - roQuery: RO_QUERY, - SLOWLOG, - slowLog: SLOWLOG -} as const satisfies RedisCommands; diff --git a/packages/graph/lib/graph.spec.ts b/packages/graph/lib/graph.spec.ts deleted file mode 100644 index ab506c43a4b..00000000000 --- a/packages/graph/lib/graph.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { strict as assert } from 'node:assert'; -import testUtils, { GLOBAL } from './test-utils'; -import Graph from './graph'; - -describe('Graph', () => { - testUtils.testWithClient('null', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN null AS key'); - - assert.deepEqual( - data, - [{ key: null }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('string', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN "string" AS key'); - - assert.deepEqual( - data, - [{ key: 'string' }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('integer', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN 0 AS key'); - - assert.deepEqual( - data, - [{ key: 0 }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('boolean', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN false AS key'); - - assert.deepEqual( - data, - [{ key: false }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('double', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN 0.1 AS key'); - - assert.deepEqual( - data, - [{ key: 0.1 }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('array', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN [null] AS key'); - - assert.deepEqual( - data, - [{ key: [null] }] - ); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('edge', async client => { - const graph = new Graph(client as any, 'graph'); - - // check with and without metadata cache - for (let i = 0; i < 2; i++) { - const { data } = await graph.query('CREATE ()-[edge :edge]->() RETURN edge'); - assert.ok(Array.isArray(data)); - assert.equal(data.length, 1); - assert.equal(typeof data[0].edge.id, 'number'); - assert.equal(data[0].edge.relationshipType, 'edge'); - assert.equal(typeof data[0].edge.sourceId, 'number'); - assert.equal(typeof data[0].edge.destinationId, 'number'); - assert.deepEqual(data[0].edge.properties, {}); - } - - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('node', async client => { - const graph = new Graph(client as any, 'graph'); - - // check with and without metadata cache - for (let i = 0; i < 2; i++) { - const { data } = await graph.query('CREATE (node :node { p: 0 }) RETURN node'); - assert.ok(Array.isArray(data)); - assert.equal(data.length, 1); - assert.equal(typeof data[0].node.id, 'number'); - assert.deepEqual(data[0].node.labels, ['node']); - assert.deepEqual(data[0].node.properties, { p: 0 }); - } - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('path', async client => { - const graph = new Graph(client as any, 'graph'), - [, { data }] = await Promise.all([ - await graph.query('CREATE ()-[:edge]->()'), - await graph.roQuery('MATCH path = ()-[:edge]->() RETURN path') - ]); - - assert.ok(Array.isArray(data)); - assert.equal(data.length, 1); - - assert.ok(Array.isArray(data[0].path.nodes)); - assert.equal(data[0].path.nodes.length, 2); - for (const node of data[0].path.nodes) { - assert.equal(typeof node.id, 'number'); - assert.deepEqual(node.labels, []); - assert.deepEqual(node.properties, {}); - } - - assert.ok(Array.isArray(data[0].path.edges)); - assert.equal(data[0].path.edges.length, 1); - for (const edge of data[0].path.edges) { - assert.equal(typeof edge.id, 'number'); - assert.equal(edge.relationshipType, 'edge'); - assert.equal(typeof edge.sourceId, 'number'); - assert.equal(typeof edge.destinationId, 'number'); - assert.deepEqual(edge.properties, {}); - } - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('map', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN { key: "value" } AS map'); - - assert.deepEqual(data, [{ - map: { - key: 'value' - } - }]); - }, GLOBAL.SERVERS.OPEN); - - testUtils.testWithClient('point', async client => { - const graph = new Graph(client as any, 'graph'), - { data } = await graph.query('RETURN point({ latitude: 1, longitude: 2 }) AS point'); - - assert.deepEqual(data, [{ - point: { - latitude: 1, - longitude: 2 - } - }]); - }, GLOBAL.SERVERS.OPEN); -}); diff --git a/packages/graph/lib/graph.ts b/packages/graph/lib/graph.ts deleted file mode 100644 index 348c8b7155f..00000000000 --- a/packages/graph/lib/graph.ts +++ /dev/null @@ -1,359 +0,0 @@ -import { RedisClientType } from '@redis/client'; -import { RedisArgument, RedisFunctions, RedisScripts } from '@redis/client/dist/lib/RESP/types'; -import QUERY, { QueryOptions } from './commands/QUERY'; - -interface GraphMetadata { - labels: Array; - relationshipTypes: Array; - propertyKeys: Array; -} - -// https://github.com/RedisGraph/RedisGraph/blob/master/src/resultset/formatters/resultset_formatter.h#L20 -enum GraphValueTypes { - UNKNOWN = 0, - NULL = 1, - STRING = 2, - INTEGER = 3, - BOOLEAN = 4, - DOUBLE = 5, - ARRAY = 6, - EDGE = 7, - NODE = 8, - PATH = 9, - MAP = 10, - POINT = 11 -} - -type GraphEntityRawProperties = Array<[ - id: number, - ...value: GraphRawValue -]>; - -type GraphEdgeRawValue = [ - GraphValueTypes.EDGE, - [ - id: number, - relationshipTypeId: number, - sourceId: number, - destinationId: number, - properties: GraphEntityRawProperties - ] -]; - -type GraphNodeRawValue = [ - GraphValueTypes.NODE, - [ - id: number, - labelIds: Array, - properties: GraphEntityRawProperties - ] -]; - -type GraphPathRawValue = [ - GraphValueTypes.PATH, - [ - nodes: [ - GraphValueTypes.ARRAY, - Array - ], - edges: [ - GraphValueTypes.ARRAY, - Array - ] - ] -]; - -type GraphMapRawValue = [ - GraphValueTypes.MAP, - Array -]; - -type GraphRawValue = [ - GraphValueTypes.NULL, - null -] | [ - GraphValueTypes.STRING, - string -] | [ - GraphValueTypes.INTEGER, - number -] | [ - GraphValueTypes.BOOLEAN, - string -] | [ - GraphValueTypes.DOUBLE, - string -] | [ - GraphValueTypes.ARRAY, - Array -] | GraphEdgeRawValue | GraphNodeRawValue | GraphPathRawValue | GraphMapRawValue | [ - GraphValueTypes.POINT, - [ - latitude: string, - longitude: string - ] -]; - -type GraphEntityProperties = Record; - -interface GraphEdge { - id: number; - relationshipType: string; - sourceId: number; - destinationId: number; - properties: GraphEntityProperties; -} - -interface GraphNode { - id: number; - labels: Array; - properties: GraphEntityProperties; -} - -interface GraphPath { - nodes: Array; - edges: Array; -} - -type GraphMap = { - [key: string]: GraphValue; -}; - -type GraphValue = null | string | number | boolean | Array | { -} | GraphEdge | GraphNode | GraphPath | GraphMap | { - latitude: string; - longitude: string; -}; - -export type GraphReply = { - data?: Array; -}; - -export type GraphClientType = RedisClientType<{ - graph: { - query: typeof QUERY, - roQuery: typeof import('./commands/RO_QUERY.js').default - } -}, RedisFunctions, RedisScripts>; - -export default class Graph { - #client: GraphClientType; - #name: string; - #metadata?: GraphMetadata; - - constructor( - client: GraphClientType, - name: string - ) { - this.#client = client; - this.#name = name; - } - - async query( - query: RedisArgument, - options?: QueryOptions - ) { - return this.#parseReply( - await this.#client.graph.query( - this.#name, - query, - options, - true - ) - ); - } - - async roQuery( - query: RedisArgument, - options?: QueryOptions - ) { - return this.#parseReply( - await this.#client.graph.roQuery( - this.#name, - query, - options, - true - ) - ); - } - - #setMetadataPromise?: Promise; - - #updateMetadata(): Promise { - this.#setMetadataPromise ??= this.#setMetadata() - .finally(() => this.#setMetadataPromise = undefined); - return this.#setMetadataPromise; - } - - // DO NOT use directly, use #updateMetadata instead - async #setMetadata(): Promise { - const [labels, relationshipTypes, propertyKeys] = await Promise.all([ - this.#client.graph.roQuery(this.#name, 'CALL db.labels()'), - this.#client.graph.roQuery(this.#name, 'CALL db.relationshipTypes()'), - this.#client.graph.roQuery(this.#name, 'CALL db.propertyKeys()') - ]); - - this.#metadata = { - labels: this.#cleanMetadataArray(labels.data as Array<[string]>), - relationshipTypes: this.#cleanMetadataArray(relationshipTypes.data as Array<[string]>), - propertyKeys: this.#cleanMetadataArray(propertyKeys.data as Array<[string]>) - }; - - return this.#metadata; - } - - #cleanMetadataArray(arr: Array<[string]>): Array { - return arr.map(([value]) => value); - } - - #getMetadata( - key: T, - id: number - ): GraphMetadata[T][number] | Promise { - return this.#metadata?.[key][id] ?? this.#getMetadataAsync(key, id); - } - - // DO NOT use directly, use #getMetadata instead - async #getMetadataAsync( - key: T, - id: number - ): Promise { - const value = (await this.#updateMetadata())[key][id]; - if (value === undefined) throw new Error(`Cannot find value from ${key}[${id}]`); - return value; - } - - // TODO: reply type - async #parseReply(reply: any): Promise> { - if (!reply.data) return reply; - - const promises: Array> = [], - parsed = { - metadata: reply.metadata, - data: reply.data!.map((row: any) => { - const data: Record = {}; - for (let i = 0; i < row.length; i++) { - data[reply.headers[i][1]] = this.#parseValue(row[i], promises); - } - - return data as unknown as T; - }) - }; - - if (promises.length) await Promise.all(promises); - - return parsed; - } - - #parseValue([valueType, value]: GraphRawValue, promises: Array>): GraphValue { - switch (valueType) { - case GraphValueTypes.NULL: - return null; - - case GraphValueTypes.STRING: - case GraphValueTypes.INTEGER: - return value; - - case GraphValueTypes.BOOLEAN: - return value === 'true'; - - case GraphValueTypes.DOUBLE: - return parseFloat(value); - - case GraphValueTypes.ARRAY: - return value.map(x => this.#parseValue(x, promises)); - - case GraphValueTypes.EDGE: - return this.#parseEdge(value, promises); - - case GraphValueTypes.NODE: - return this.#parseNode(value, promises); - - case GraphValueTypes.PATH: - return { - nodes: value[0][1].map(([, node]) => this.#parseNode(node, promises)), - edges: value[1][1].map(([, edge]) => this.#parseEdge(edge, promises)) - }; - - case GraphValueTypes.MAP: - const map: GraphMap = {}; - for (let i = 0; i < value.length; i++) { - map[value[i++] as string] = this.#parseValue(value[i] as GraphRawValue, promises); - } - - return map; - - case GraphValueTypes.POINT: - return { - latitude: parseFloat(value[0]), - longitude: parseFloat(value[1]) - }; - - default: - throw new Error(`unknown scalar type: ${valueType}`); - } - } - - #parseEdge([ - id, - relationshipTypeId, - sourceId, - destinationId, - properties - ]: GraphEdgeRawValue[1], promises: Array>): GraphEdge { - const edge = { - id, - sourceId, - destinationId, - properties: this.#parseProperties(properties, promises) - } as GraphEdge; - - const relationshipType = this.#getMetadata('relationshipTypes', relationshipTypeId); - if (relationshipType instanceof Promise) { - promises.push( - relationshipType.then(value => edge.relationshipType = value) - ); - } else { - edge.relationshipType = relationshipType; - } - - return edge; - } - - #parseNode([ - id, - labelIds, - properties - ]: GraphNodeRawValue[1], promises: Array>): GraphNode { - const labels = new Array(labelIds.length); - for (let i = 0; i < labelIds.length; i++) { - const value = this.#getMetadata('labels', labelIds[i]); - if (value instanceof Promise) { - promises.push(value.then(value => labels[i] = value)); - } else { - labels[i] = value; - } - } - - return { - id, - labels, - properties: this.#parseProperties(properties, promises) - }; - } - - #parseProperties(raw: GraphEntityRawProperties, promises: Array>): GraphEntityProperties { - const parsed: GraphEntityProperties = {}; - for (const [id, type, value] of raw) { - const parsedValue = this.#parseValue([type, value] as GraphRawValue, promises), - key = this.#getMetadata('propertyKeys', id); - if (key instanceof Promise) { - promises.push(key.then(key => parsed[key] = parsedValue)); - } else { - parsed[key] = parsedValue; - } - } - - return parsed; - } -} diff --git a/packages/graph/lib/index.ts b/packages/graph/lib/index.ts deleted file mode 100644 index e9f15ab1fd9..00000000000 --- a/packages/graph/lib/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default } from './commands'; -export { default as Graph } from './graph'; diff --git a/packages/graph/lib/test-utils.ts b/packages/graph/lib/test-utils.ts deleted file mode 100644 index 16c44582061..00000000000 --- a/packages/graph/lib/test-utils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import TestUtils from '@redis/test-utils'; -import RedisGraph from '.'; - - -export default TestUtils.createFromConfig({ - dockerImageName: 'redislabs/client-libs-test', - dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M04-pre' -}); - -export const GLOBAL = { - SERVERS: { - OPEN: { - serverArguments: [], - clientOptions: { - modules: { - graph: RedisGraph - } - } - } - } -}; diff --git a/packages/graph/package.json b/packages/graph/package.json deleted file mode 100644 index 1ea08401f1b..00000000000 --- a/packages/graph/package.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "@redis/graph", - "version": "5.0.0-next.6", - "license": "MIT", - "main": "./dist/lib/index.js", - "types": "./dist/lib/index.d.ts", - "files": [ - "dist/", - "!dist/tsconfig.tsbuildinfo" - ], - "scripts": { - "test-disable": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" - }, - "peerDependencies": { - "@redis/client": "^5.0.0-next.6" - }, - "devDependencies": { - "@redis/test-utils": "*" - }, - "engines": { - "node": ">= 18" - }, - "repository": { - "type": "git", - "url": "git://github.com/redis/node-redis.git" - }, - "bugs": { - "url": "https://github.com/redis/node-redis/issues" - }, - "homepage": "https://github.com/redis/node-redis/tree/master/packages/graph", - "keywords": [ - "redis", - "RedisGraph" - ] -} diff --git a/packages/graph/tsconfig.json b/packages/graph/tsconfig.json deleted file mode 100644 index 9d17cb63371..00000000000 --- a/packages/graph/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { - "outDir": "./dist" - }, - "include": [ - "./lib/**/*.ts" - ], - "exclude": [ - "./lib/test-utils.ts", - "./lib/**/*.spec.ts" - ], - "typedocOptions": { - "entryPoints": [ - "./lib" - ], - "entryPointStrategy": "expand", - "out": "../../documentation/graph" - } -} diff --git a/packages/redis/index.ts b/packages/redis/index.ts index 572b45b707b..73477363d3b 100644 --- a/packages/redis/index.ts +++ b/packages/redis/index.ts @@ -15,21 +15,18 @@ import { createSentinel as genericCreateSentinel } from '@redis/client'; import RedisBloomModules from '@redis/bloom'; -import RedisGraph from '@redis/graph'; import RedisJSON from '@redis/json'; import RediSearch from '@redis/search'; import RedisTimeSeries from '@redis/time-series'; // export * from '@redis/client'; // export * from '@redis/bloom'; -// export * from '@redis/graph'; // export * from '@redis/json'; // export * from '@redis/search'; // export * from '@redis/time-series'; const modules = { ...RedisBloomModules, - graph: RedisGraph, json: RedisJSON, ft: RediSearch, ts: RedisTimeSeries diff --git a/packages/redis/package.json b/packages/redis/package.json index 5069d936ba8..c9719370e88 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -12,7 +12,6 @@ "dependencies": { "@redis/bloom": "5.0.0-next.6", "@redis/client": "5.0.0-next.6", - "@redis/graph": "5.0.0-next.6", "@redis/json": "5.0.0-next.6", "@redis/search": "5.0.0-next.6", "@redis/time-series": "5.0.0-next.6" diff --git a/tsconfig.json b/tsconfig.json index 8f43ab41d22..180b3fc2ba7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,9 +10,6 @@ { "path": "./packages/bloom" }, - { - "path": "./packages/graph" - }, { "path": "./packages/json" }, From a7feb60e0af755b706a09208fcacbc553d6327c3 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Tue, 18 Mar 2025 15:24:11 +0200 Subject: [PATCH 045/105] tests: bumped the version of the 8 docker test image to '8.0-M05-pre' (#2909) --- .github/workflows/tests.yml | 2 +- packages/bloom/lib/test-utils.ts | 2 +- packages/client/lib/test-utils.ts | 2 +- packages/entraid/lib/test-utils.ts | 2 +- packages/json/lib/test-utils.ts | 2 +- packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts | 6 ++---- packages/search/lib/test-utils.ts | 2 +- packages/test-utils/lib/index.ts | 3 +++ packages/time-series/lib/test-utils.ts | 2 +- 9 files changed, 12 insertions(+), 11 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7bcc72e5408..366a24b4dc6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: node-version: [ '18', '20', '22' ] - redis-version: [ 'rs-7.2.0-v13', 'rs-7.4.0-v1', '8.0-M04-pre' ] + redis-version: [ 'rs-7.2.0-v13', 'rs-7.4.0-v1', '8.0-M05-pre' ] steps: - uses: actions/checkout@v4 with: diff --git a/packages/bloom/lib/test-utils.ts b/packages/bloom/lib/test-utils.ts index 7996d61e44e..71b423b41ea 100644 --- a/packages/bloom/lib/test-utils.ts +++ b/packages/bloom/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisBloomModules from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M04-pre' + defaultDockerVersion: '8.0-M05-pre' }); export const GLOBAL = { diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index dce1f97d88a..f7862a9d685 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -8,7 +8,7 @@ import { BasicCommandParser } from './client/parser'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M04-pre' + defaultDockerVersion: '8.0-M05-pre' }); export default utils; diff --git a/packages/entraid/lib/test-utils.ts b/packages/entraid/lib/test-utils.ts index 970637a1225..11ad498f0b3 100644 --- a/packages/entraid/lib/test-utils.ts +++ b/packages/entraid/lib/test-utils.ts @@ -6,7 +6,7 @@ import { EntraidCredentialsProvider } from './entraid-credentials-provider'; export const testUtils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M04-pre' + defaultDockerVersion: '8.0-M05-pre' }); const DEBUG_MODE_ARGS = testUtils.isVersionGreaterThan([7]) ? diff --git a/packages/json/lib/test-utils.ts b/packages/json/lib/test-utils.ts index caa1c3049af..9894b2d0399 100644 --- a/packages/json/lib/test-utils.ts +++ b/packages/json/lib/test-utils.ts @@ -4,7 +4,7 @@ import RedisJSON from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M04-pre' + defaultDockerVersion: '8.0-M05-pre' }); export const GLOBAL = { diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts index bdf452c16ea..ca2302f08ea 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts @@ -59,7 +59,7 @@ describe('PROFILE AGGREGATE', () => { assert.ok(shardProfile.includes('Warning')); assert.ok(shardProfile.includes('Iterators profile')); - }, GLOBAL.SERVERS.OPEN); + }, Object.assign(GLOBAL.SERVERS.OPEN, {skipTest: true})); testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 4, 0]], 'client.ft.search', async client => { await Promise.all([ @@ -106,7 +106,5 @@ describe('PROFILE AGGREGATE', () => { const normalizedRes = normalizeObject(res); assert.equal(normalizedRes.Results.total_results, 1); assert.ok(normalizedRes.Profile.Shards); - - }, GLOBAL.SERVERS.OPEN_3) - + }, Object.assign(GLOBAL.SERVERS.OPEN_3, {skipTest: true})); }); diff --git a/packages/search/lib/test-utils.ts b/packages/search/lib/test-utils.ts index 1318676042e..7264b1b6b12 100644 --- a/packages/search/lib/test-utils.ts +++ b/packages/search/lib/test-utils.ts @@ -5,7 +5,7 @@ import { RespVersions } from '@redis/client'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M04-pre' + defaultDockerVersion: '8.0-M05-pre' }); export const GLOBAL = { diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index b48f11b02c7..1c564749ff2 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -54,6 +54,7 @@ interface TestUtilsConfig { interface CommonTestOptions { serverArguments: Array; minimumDockerVersion?: Array; + skipTest?: boolean; } interface ClientTestOptions< @@ -242,6 +243,7 @@ export default class TestUtils { } it(title, async function () { + if (options.skipTest) return this.skip(); if (!dockerPromise) return this.skip(); const client = createClient({ @@ -316,6 +318,7 @@ export default class TestUtils { } it(title, async function () { + if (options.skipTest) return this.skip(); if (!dockerPromise) return this.skip(); const pool = createClientPool({ diff --git a/packages/time-series/lib/test-utils.ts b/packages/time-series/lib/test-utils.ts index 0b7e940788f..0f25341e34d 100644 --- a/packages/time-series/lib/test-utils.ts +++ b/packages/time-series/lib/test-utils.ts @@ -4,7 +4,7 @@ import TimeSeries from '.'; export default TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', - defaultDockerVersion: '8.0-M04-pre' + defaultDockerVersion: '8.0-M05-pre' }); export const GLOBAL = { From 2ff5cb88d4ab1792d6ca0b613f69dfb424a92d8e Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Wed, 19 Mar 2025 12:18:30 +0200 Subject: [PATCH 046/105] tests: Reenable and fix profile aggregate tests (#2910) --- packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts index ca2302f08ea..dbb834ed7c2 100644 --- a/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts +++ b/packages/search/lib/commands/PROFILE_AGGREGATE.spec.ts @@ -45,7 +45,7 @@ describe('PROFILE AGGREGATE', () => { const res = await client.ft.profileAggregate('index', '*'); const normalizedRes = normalizeObject(res); - assert.equal(normalizedRes.results.total, 1); + assert.equal(normalizedRes.results.total, 2); assert.ok(normalizedRes.profile[0] === 'Shards'); assert.ok(Array.isArray(normalizedRes.profile[1])); @@ -59,7 +59,7 @@ describe('PROFILE AGGREGATE', () => { assert.ok(shardProfile.includes('Warning')); assert.ok(shardProfile.includes('Iterators profile')); - }, Object.assign(GLOBAL.SERVERS.OPEN, {skipTest: true})); + }, GLOBAL.SERVERS.OPEN); testUtils.testWithClientIfVersionWithinRange([[7, 2, 0], [7, 4, 0]], 'client.ft.search', async client => { await Promise.all([ @@ -104,7 +104,7 @@ describe('PROFILE AGGREGATE', () => { const res = await client.ft.profileAggregate('index', '*'); const normalizedRes = normalizeObject(res); - assert.equal(normalizedRes.Results.total_results, 1); + assert.equal(normalizedRes.Results.total_results, 2); assert.ok(normalizedRes.Profile.Shards); - }, Object.assign(GLOBAL.SERVERS.OPEN_3, {skipTest: true})); + }, GLOBAL.SERVERS.OPEN_3); }); From 4cbecf6a0911d7f3b3d3d7864105d8912739ee99 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Wed, 19 Mar 2025 12:26:23 +0200 Subject: [PATCH 047/105] feat(hash field expiration): Added hash field expiration commands (#2907) * [CAE-686] Added hash field expiration commands * [CAE-686] Improve HSETEX return type * [CAE-686] Minor pushTuples change, renamed HSETEX test * [CAE-686] Changed hsetex function signature for better consistency with other commands * [CAE-686] Fixed hsetex test * [CAE-686] Bumped docker version to 8.0-M05-pre, enabled and fixed tests --- packages/client/lib/commands/HGETDEL.spec.ts | 48 ++++++++ packages/client/lib/commands/HGETDEL.ts | 13 +++ packages/client/lib/commands/HGETEX.spec.ts | 78 +++++++++++++ packages/client/lib/commands/HGETEX.ts | 42 +++++++ packages/client/lib/commands/HSETEX.spec.ts | 98 ++++++++++++++++ packages/client/lib/commands/HSETEX.ts | 110 ++++++++++++++++++ .../lib/commands/generic-transformers.ts | 2 +- packages/client/lib/commands/index.ts | 9 ++ 8 files changed, 399 insertions(+), 1 deletion(-) create mode 100644 packages/client/lib/commands/HGETDEL.spec.ts create mode 100644 packages/client/lib/commands/HGETDEL.ts create mode 100644 packages/client/lib/commands/HGETEX.spec.ts create mode 100644 packages/client/lib/commands/HGETEX.ts create mode 100644 packages/client/lib/commands/HSETEX.spec.ts create mode 100644 packages/client/lib/commands/HSETEX.ts diff --git a/packages/client/lib/commands/HGETDEL.spec.ts b/packages/client/lib/commands/HGETDEL.spec.ts new file mode 100644 index 00000000000..b2e19967f1d --- /dev/null +++ b/packages/client/lib/commands/HGETDEL.spec.ts @@ -0,0 +1,48 @@ +import { strict as assert } from 'node:assert'; +import testUtils, { GLOBAL } from '../test-utils'; +import { BasicCommandParser } from '../client/parser'; +import HGETDEL from './HGETDEL'; + +describe('HGETDEL parseCommand', () => { + it('hGetDel parseCommand base', () => { + const parser = new BasicCommandParser; + HGETDEL.parseCommand(parser, 'key', 'field'); + assert.deepEqual(parser.redisArgs, ['HGETDEL', 'key', 'FIELDS', '1', 'field']); + }); + + it('hGetDel parseCommand variadic', () => { + const parser = new BasicCommandParser; + HGETDEL.parseCommand(parser, 'key', ['field1', 'field2']); + assert.deepEqual(parser.redisArgs, ['HGETDEL', 'key', 'FIELDS', '2', 'field1', 'field2']); + }); +}); + + +describe('HGETDEL call', () => { + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel empty single field', async client => { + assert.deepEqual( + await client.hGetDel('key', 'filed1'), + [null] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel empty multiple fields', async client => { + assert.deepEqual( + await client.hGetDel('key', ['filed1', 'field2']), + [null, null] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetDel partially populated multiple fields', async client => { + await client.hSet('key', 'field1', 'value1') + assert.deepEqual( + await client.hGetDel('key', ['field1', 'field2']), + ['value1', null] + ); + + assert.deepEqual( + await client.hGetDel('key', 'field1'), + [null] + ); + }, GLOBAL.SERVERS.OPEN); +}); diff --git a/packages/client/lib/commands/HGETDEL.ts b/packages/client/lib/commands/HGETDEL.ts new file mode 100644 index 00000000000..a0326c425ea --- /dev/null +++ b/packages/client/lib/commands/HGETDEL.ts @@ -0,0 +1,13 @@ +import { CommandParser } from '../client/parser'; +import { RedisVariadicArgument } from './generic-transformers'; +import { RedisArgument, ArrayReply, BlobStringReply, NullReply, Command } from '../RESP/types'; + +export default { + parseCommand(parser: CommandParser, key: RedisArgument, fields: RedisVariadicArgument) { + parser.push('HGETDEL'); + parser.pushKey(key); + parser.push('FIELDS') + parser.pushVariadicWithLength(fields); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HGETEX.spec.ts b/packages/client/lib/commands/HGETEX.spec.ts new file mode 100644 index 00000000000..2625a0ac023 --- /dev/null +++ b/packages/client/lib/commands/HGETEX.spec.ts @@ -0,0 +1,78 @@ +import { strict as assert } from 'node:assert'; +import testUtils,{ GLOBAL } from '../test-utils'; +import { BasicCommandParser } from '../client/parser'; +import HGETEX from './HGETEX'; +import { setTimeout } from 'timers/promises'; + +describe('HGETEX parseCommand', () => { + it('hGetEx parseCommand base', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', 'field'); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'FIELDS', '1', 'field']); + }); + + it('hGetEx parseCommand expiration PERSIST string', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', 'field', {expiration: 'PERSIST'}); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'PERSIST', 'FIELDS', '1', 'field']); + }); + + it('hGetEx parseCommand expiration PERSIST obj', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', 'field', {expiration: {type: 'PERSIST'}}); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'PERSIST', 'FIELDS', '1', 'field']); + }); + + it('hGetEx parseCommand expiration EX obj', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', 'field', {expiration: {type: 'EX', value: 1000}}); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'EX', '1000', 'FIELDS', '1', 'field']); + }); + + it('hGetEx parseCommand expiration EXAT obj variadic', () => { + const parser = new BasicCommandParser; + HGETEX.parseCommand(parser, 'key', ['field1', 'field2'], {expiration: {type: 'EXAT', value: 1000}}); + assert.deepEqual(parser.redisArgs, ['HGETEX', 'key', 'EXAT', '1000', 'FIELDS', '2', 'field1', 'field2']); + }); +}); + + +describe('HGETEX call', () => { + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx empty single field', async client => { + assert.deepEqual( + await client.hGetEx('key', 'field1', {expiration: 'PERSIST'}), + [null] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx empty multiple fields', async client => { + assert.deepEqual( + await client.hGetEx('key', ['field1', 'field2'], {expiration: 'PERSIST'}), + [null, null] + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hGetEx set expiry', async client => { + await client.hSet('key', 'field', 'value') + assert.deepEqual( + await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 50}}), + ['value'] + ); + await setTimeout(100) + assert.deepEqual( + await client.hGet('key', 'field'), + null + ); + }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'gGetEx set expiry PERSIST', async client => { + await client.hSet('key', 'field', 'value') + await client.hGetEx('key', 'field', {expiration: {type: 'PX', value: 50}}) + await client.hGetEx('key', 'field', {expiration: 'PERSIST'}) + await setTimeout(100) + assert.deepEqual( + await client.hGet('key', 'field'), + 'value' + ) + }, GLOBAL.SERVERS.OPEN); +}); \ No newline at end of file diff --git a/packages/client/lib/commands/HGETEX.ts b/packages/client/lib/commands/HGETEX.ts new file mode 100644 index 00000000000..ce265e15bd6 --- /dev/null +++ b/packages/client/lib/commands/HGETEX.ts @@ -0,0 +1,42 @@ +import { CommandParser } from '../client/parser'; +import { RedisVariadicArgument } from './generic-transformers'; +import { ArrayReply, Command, BlobStringReply, NullReply, RedisArgument } from '../RESP/types'; + +export interface HGetExOptions { + expiration?: { + type: 'EX' | 'PX' | 'EXAT' | 'PXAT'; + value: number; + } | { + type: 'PERSIST'; + } | 'PERSIST'; +} + +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: RedisVariadicArgument, + options?: HGetExOptions + ) { + parser.push('HGETEX'); + parser.pushKey(key); + + if (options?.expiration) { + if (typeof options.expiration === 'string') { + parser.push(options.expiration); + } else if (options.expiration.type === 'PERSIST') { + parser.push('PERSIST'); + } else { + parser.push( + options.expiration.type, + options.expiration.value.toString() + ); + } + } + + parser.push('FIELDS') + + parser.pushVariadicWithLength(fields); + }, + transformReply: undefined as unknown as () => ArrayReply +} as const satisfies Command; diff --git a/packages/client/lib/commands/HSETEX.spec.ts b/packages/client/lib/commands/HSETEX.spec.ts new file mode 100644 index 00000000000..fc38e0f0f45 --- /dev/null +++ b/packages/client/lib/commands/HSETEX.spec.ts @@ -0,0 +1,98 @@ +import { strict as assert } from 'node:assert'; +import testUtils,{ GLOBAL } from '../test-utils'; +import { BasicCommandParser } from '../client/parser'; +import HSETEX from './HSETEX'; + +describe('HSETEX parseCommand', () => { + it('hSetEx parseCommand base', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', ['field', 'value']); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '1', 'field', 'value']); + }); + + it('hSetEx parseCommand base empty obj', () => { + const parser = new BasicCommandParser; + assert.throws(() => {HSETEX.parseCommand(parser, 'key', {})}); + }); + + it('hSetEx parseCommand base one key obj', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', {'k': 'v'}); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '1', 'k', 'v']); + }); + + it('hSetEx parseCommand array', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', ['field1', 'value1', 'field2', 'value2']); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); + }); + + it('hSetEx parseCommand array invalid args, throws an error', () => { + const parser = new BasicCommandParser; + assert.throws(() => {HSETEX.parseCommand(parser, 'key', ['field1', 'value1', 'field2'])}); + }); + + it('hSetEx parseCommand array in array', () => { + const parser1 = new BasicCommandParser; + HSETEX.parseCommand(parser1, 'key', [['field1', 'value1'], ['field2', 'value2']]); + assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); + + const parser2 = new BasicCommandParser; + HSETEX.parseCommand(parser2, 'key', [['field1', 'value1'], ['field2', 'value2'], ['field3', 'value3']]); + assert.deepEqual(parser2.redisArgs, ['HSETEX', 'key', 'FIELDS', '3', 'field1', 'value1', 'field2', 'value2', 'field3', 'value3']); + }); + + it('hSetEx parseCommand map', () => { + const parser1 = new BasicCommandParser; + HSETEX.parseCommand(parser1, 'key', new Map([['field1', 'value1'], ['field2', 'value2']])); + assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); + }); + + it('hSetEx parseCommand obj', () => { + const parser1 = new BasicCommandParser; + HSETEX.parseCommand(parser1, 'key', {field1: "value1", field2: "value2"}); + assert.deepEqual(parser1.redisArgs, ['HSETEX', 'key', 'FIELDS', '2', 'field1', 'value1', 'field2', 'value2']); + }); + + it('hSetEx parseCommand options FNX KEEPTTL', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', ['field', 'value'], {mode: 'FNX', expiration: 'KEEPTTL'}); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FNX', 'KEEPTTL', 'FIELDS', '1', 'field', 'value']); + }); + + it('hSetEx parseCommand options FXX EX 500', () => { + const parser = new BasicCommandParser; + HSETEX.parseCommand(parser, 'key', ['field', 'value'], {mode: 'FXX', expiration: {type: 'EX', value: 500}}); + assert.deepEqual(parser.redisArgs, ['HSETEX', 'key', 'FXX', 'EX', '500', 'FIELDS', '1', 'field', 'value']); + }); +}); + + +describe('HSETEX call', () => { + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'hSetEx calls', async client => { + assert.deepEqual( + await client.hSetEx('key_hsetex_call', ['field1', 'value1'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), + 1 + ); + + assert.deepEqual( + await client.hSetEx('key_hsetex_call', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}), + 0 + ); + + assert.deepEqual( + await client.hSetEx('key_hsetex_call', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), + 0 + ); + + assert.deepEqual( + await client.hSetEx('key_hsetex_call', ['field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FNX"}), + 1 + ); + + assert.deepEqual( + await client.hSetEx('key_hsetex_call', ['field1', 'value1', 'field2', 'value2'], {expiration: {type: "EX", value: 500}, mode: "FXX"}), + 1 + ); + }, GLOBAL.SERVERS.OPEN); +}); \ No newline at end of file diff --git a/packages/client/lib/commands/HSETEX.ts b/packages/client/lib/commands/HSETEX.ts new file mode 100644 index 00000000000..3827538934c --- /dev/null +++ b/packages/client/lib/commands/HSETEX.ts @@ -0,0 +1,110 @@ +import { BasicCommandParser, CommandParser } from '../client/parser'; +import { Command, NumberReply, RedisArgument } from '../RESP/types'; + +export interface HSetExOptions { + expiration?: { + type: 'EX' | 'PX' | 'EXAT' | 'PXAT'; + value: number; + } | { + type: 'KEEPTTL'; + } | 'KEEPTTL'; + mode?: 'FNX' | 'FXX' + } + +export type HashTypes = RedisArgument | number; + +type HSETEXObject = Record; + +type HSETEXMap = Map; + +type HSETEXTuples = Array<[HashTypes, HashTypes]> | Array; + +export default { + parseCommand( + parser: CommandParser, + key: RedisArgument, + fields: HSETEXObject | HSETEXMap | HSETEXTuples, + options?: HSetExOptions + ) { + parser.push('HSETEX'); + parser.pushKey(key); + + if (options?.mode) { + parser.push(options.mode) + } + if (options?.expiration) { + if (typeof options.expiration === 'string') { + parser.push(options.expiration); + } else if (options.expiration.type === 'KEEPTTL') { + parser.push('KEEPTTL'); + } else { + parser.push( + options.expiration.type, + options.expiration.value.toString() + ); + } + } + + parser.push('FIELDS') + if (fields instanceof Map) { + pushMap(parser, fields); + } else if (Array.isArray(fields)) { + pushTuples(parser, fields); + } else { + pushObject(parser, fields); + } + }, + transformReply: undefined as unknown as () => NumberReply<0 | 1> +} as const satisfies Command; + + +function pushMap(parser: CommandParser, map: HSETEXMap): void { + parser.push(map.size.toString()) + for (const [key, value] of map.entries()) { + parser.push( + convertValue(key), + convertValue(value) + ); + } +} + +function pushTuples(parser: CommandParser, tuples: HSETEXTuples): void { + const tmpParser = new BasicCommandParser + _pushTuples(tmpParser, tuples) + + if (tmpParser.redisArgs.length%2 != 0) { + throw Error('invalid number of arguments, expected key value ....[key value] pairs, got key without value') + } + + parser.push((tmpParser.redisArgs.length/2).toString()) + parser.push(...tmpParser.redisArgs) +} + +function _pushTuples(parser: CommandParser, tuples: HSETEXTuples): void { + for (const tuple of tuples) { + if (Array.isArray(tuple)) { + _pushTuples(parser, tuple); + continue; + } + parser.push(convertValue(tuple)); + } +} + +function pushObject(parser: CommandParser, object: HSETEXObject): void { + const len = Object.keys(object).length + if (len == 0) { + throw Error('object without keys') + } + + parser.push(len.toString()) + for (const key of Object.keys(object)) { + parser.push( + convertValue(key), + convertValue(object[key]) + ); + } +} + +function convertValue(value: HashTypes): RedisArgument { + return typeof value === 'number' ? value.toString() : value; +} \ No newline at end of file diff --git a/packages/client/lib/commands/generic-transformers.ts b/packages/client/lib/commands/generic-transformers.ts index fc139a948e0..91eab7107a1 100644 --- a/packages/client/lib/commands/generic-transformers.ts +++ b/packages/client/lib/commands/generic-transformers.ts @@ -269,7 +269,7 @@ export function pushVariadicArgument( return args; } -export function parseOptionalVariadicArgument( +export function parseOptionalVariadicArgument( parser: CommandParser, name: RedisArgument, value?: RedisVariadicArgument diff --git a/packages/client/lib/commands/index.ts b/packages/client/lib/commands/index.ts index 024ee2191b8..5cd81331a4e 100644 --- a/packages/client/lib/commands/index.ts +++ b/packages/client/lib/commands/index.ts @@ -138,6 +138,8 @@ import HEXPIREAT from './HEXPIREAT'; import HEXPIRETIME from './HEXPIRETIME'; import HGET from './HGET'; import HGETALL from './HGETALL'; +import HGETDEL from './HGETDEL'; +import HGETEX from './HGETEX'; import HINCRBY from './HINCRBY'; import HINCRBYFLOAT from './HINCRBYFLOAT'; import HKEYS from './HKEYS'; @@ -154,6 +156,7 @@ import HRANDFIELD from './HRANDFIELD'; import HSCAN from './HSCAN'; import HSCAN_NOVALUES from './HSCAN_NOVALUES'; import HSET from './HSET'; +import HSETEX from './HSETEX'; import HSETNX from './HSETNX'; import HSTRLEN from './HSTRLEN'; import HTTL from './HTTL'; @@ -621,6 +624,10 @@ export default { hGet: HGET, HGETALL, hGetAll: HGETALL, + HGETDEL, + hGetDel: HGETDEL, + HGETEX, + hGetEx: HGETEX, HINCRBY, hIncrBy: HINCRBY, HINCRBYFLOAT, @@ -653,6 +660,8 @@ export default { hScanNoValues: HSCAN_NOVALUES, HSET, hSet: HSET, + HSETEX, + hSetEx: HSETEX, HSETNX, hSetNX: HSETNX, HSTRLEN, From 6c5a3fd0c0d51313652771389e650cb28f3e8444 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Thu, 20 Mar 2025 12:31:25 +0200 Subject: [PATCH 048/105] fix(entraid): correct package entry point structure (#2891) - Add /index.ts that re-exports all from /lib/index.ts - Preserve existing /lib/index.ts exports --- packages/entraid/README.md | 2 +- packages/entraid/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 packages/entraid/index.ts diff --git a/packages/entraid/README.md b/packages/entraid/README.md index f2212848455..7a1ab1a94a2 100644 --- a/packages/entraid/README.md +++ b/packages/entraid/README.md @@ -39,7 +39,7 @@ The first step to using @redis/entraid is choosing the right credentials provide ```typescript import { createClient } from '@redis/client'; -import { EntraIdCredentialsProviderFactory } from '@redis/entraid/dist/lib/entra-id-credentials-provider-factory'; +import { EntraIdCredentialsProviderFactory } from '@redis/entraid'; const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({ clientId: 'your-client-id', diff --git a/packages/entraid/index.ts b/packages/entraid/index.ts new file mode 100644 index 00000000000..303b5dc6e14 --- /dev/null +++ b/packages/entraid/index.ts @@ -0,0 +1 @@ +export * from './lib/index' \ No newline at end of file From d64072da95a46331a9bd3857c221c4f889d79259 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Fri, 21 Mar 2025 11:43:10 +0200 Subject: [PATCH 049/105] feat(integer 8 vector support): Changed ft create vector types to union, added support for int8/uint8 (#2911) * [CAE-827] Changed ft create vector types to union, added support for int8/uint8 * [CAE-827] Moved test cases --- packages/search/lib/commands/CREATE.spec.ts | 83 +++++++++++++++++++++ packages/search/lib/commands/CREATE.ts | 2 +- 2 files changed, 84 insertions(+), 1 deletion(-) diff --git a/packages/search/lib/commands/CREATE.spec.ts b/packages/search/lib/commands/CREATE.spec.ts index 58888fb7ce4..2c54d3d0235 100644 --- a/packages/search/lib/commands/CREATE.spec.ts +++ b/packages/search/lib/commands/CREATE.spec.ts @@ -473,4 +473,87 @@ describe('FT.CREATE', () => { 'OK' ); }, GLOBAL.SERVERS.OPEN); + + testUtils.testWithClientIfVersionWithinRange([[7], 'LATEST'], 'client.ft.create vector types big floats', async client => { + assert.equal( + await client.ft.create("index_float32", { + field: { + ALGORITHM: "FLAT", + TYPE: "FLOAT32", + DIM: 1, + DISTANCE_METRIC: 'COSINE', + type: 'VECTOR' + }, + }), + "OK" + ); + + assert.equal( + await client.ft.create("index_float64", { + field: { + ALGORITHM: "FLAT", + TYPE: "FLOAT64", + DIM: 1, + DISTANCE_METRIC: 'COSINE', + type: 'VECTOR' + }, + }), + "OK" + ); + }, GLOBAL.SERVERS.OPEN); + + + testUtils.testWithClientIfVersionWithinRange([[8], 'LATEST'], 'client.ft.create vector types small floats and ints', async client => { + assert.equal( + await client.ft.create("index_float16", { + field: { + ALGORITHM: "FLAT", + TYPE: "FLOAT16", + DIM: 1, + DISTANCE_METRIC: 'COSINE', + type: 'VECTOR' + }, + }), + "OK" + ); + + assert.equal( + await client.ft.create("index_bloat16", { + field: { + ALGORITHM: "FLAT", + TYPE: "BFLOAT16", + DIM: 1, + DISTANCE_METRIC: 'COSINE', + type: 'VECTOR' + }, + }), + "OK" + ); + + assert.equal( + await client.ft.create("index_int8", { + field: { + ALGORITHM: "FLAT", + TYPE: "INT8", + DIM: 1, + DISTANCE_METRIC: 'COSINE', + type: 'VECTOR' + }, + }), + "OK" + ); + + assert.equal( + await client.ft.create("index_uint8", { + field: { + ALGORITHM: "FLAT", + TYPE: "UINT8", + DIM: 1, + DISTANCE_METRIC: 'COSINE', + type: 'VECTOR' + }, + }), + "OK" + ); + }, GLOBAL.SERVERS.OPEN); }); diff --git a/packages/search/lib/commands/CREATE.ts b/packages/search/lib/commands/CREATE.ts index 5eec9799264..5645a2b2dce 100644 --- a/packages/search/lib/commands/CREATE.ts +++ b/packages/search/lib/commands/CREATE.ts @@ -61,7 +61,7 @@ export type SchemaVectorFieldAlgorithm = typeof SCHEMA_VECTOR_FIELD_ALGORITHM[ke interface SchemaVectorField extends SchemaField { ALGORITHM: SchemaVectorFieldAlgorithm; - TYPE: string; + TYPE: 'FLOAT32' | 'FLOAT64' | 'BFLOAT16' | 'FLOAT16' | 'INT8' | 'UINT8'; DIM: number; DISTANCE_METRIC: 'L2' | 'IP' | 'COSINE'; INITIAL_CAP?: number; From bf06a3b703050b46c033b4b0663bf8636b06a833 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:19:53 +0200 Subject: [PATCH 050/105] Release client@5.0.0-next.7 --- packages/client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/package.json b/packages/client/package.json index cc892a12a49..3372f3a5758 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 4f88442f6fe55a2bc3a0f40828f7cef31242dc29 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:25:18 +0200 Subject: [PATCH 051/105] Updated the EntraID package to use client@5.0.0-next.7 --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index fd504cd44dc..bbf6cc363a6 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -21,7 +21,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" }, "devDependencies": { "@types/express": "^4.17.21", From 8ab820c0db6a46126e537c96ea8c004e7dac363a Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:27:10 +0200 Subject: [PATCH 052/105] Release entraid@5.0.0-next.7 --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index bbf6cc363a6..ad9cef61f4e 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 60b81ff7f061b51f8e2d38f7fad59e6cd9357fe7 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:28:15 +0200 Subject: [PATCH 053/105] Updated the JSON package to use client@5.0.0-next.7 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index b7a972c8c7a..6cdd78f6564 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" }, "devDependencies": { "@redis/test-utils": "*" From 5a2a61496eeb578ad39ce2e9821802a771addd71 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:29:09 +0200 Subject: [PATCH 054/105] Release json@5.0.0-next.7 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index 6cdd78f6564..0217aba33c6 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From c50892a7f792a3bf4cc53d6c52f834a7ea31f8f8 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:30:08 +0200 Subject: [PATCH 055/105] Updated the search package to use client@5.0.0-next.7 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index 985356cd230..1ed118daceb 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -13,7 +13,7 @@ "test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" }, "devDependencies": { "@redis/test-utils": "*" From 17b3304323653350706e713c831010e81de5e5b8 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:31:02 +0200 Subject: [PATCH 056/105] Release search@5.0.0-next.7 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index 1ed118daceb..605ab0a7ffe 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From abba2f8493f23fa1d13feaefe6fb132513a3edb9 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:31:40 +0200 Subject: [PATCH 057/105] Updated the timeseries package to use client@5.0.0-next.7 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 22cfe8cefba..665ba6222a3 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" }, "devDependencies": { "@redis/test-utils": "*" From 1b2eba2b76c2866b56e2c2501cc1a12f17f6153f Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:32:21 +0200 Subject: [PATCH 058/105] Release time-series@5.0.0-next.7 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 665ba6222a3..e0c138b0dd2 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From ffa00cfbe8015e3aaf86eedfd972011a44977d29 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:33:03 +0200 Subject: [PATCH 059/105] Updated the bloom package to use client@5.0.0-next.7 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 434ba809f7c..a3e83f09b60 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" }, "devDependencies": { "@redis/test-utils": "*" From 57e323ae16d6fca75359bd2ec85716ebff1b3aad Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:33:38 +0200 Subject: [PATCH 060/105] Release bloom@5.0.0-next.7 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index a3e83f09b60..d913d434a15 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From a3debec021f59e633407fae69fa13f3118c6d154 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:34:55 +0200 Subject: [PATCH 061/105] Updated the Redis package to use client@5.0.0-next.7 --- packages/redis/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/redis/package.json b/packages/redis/package.json index c9719370e88..b974a59ebf0 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -10,11 +10,11 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { - "@redis/bloom": "5.0.0-next.6", - "@redis/client": "5.0.0-next.6", - "@redis/json": "5.0.0-next.6", - "@redis/search": "5.0.0-next.6", - "@redis/time-series": "5.0.0-next.6" + "@redis/bloom": "5.0.0-next.7", + "@redis/client": "5.0.0-next.7", + "@redis/json": "5.0.0-next.7", + "@redis/search": "5.0.0-next.7", + "@redis/time-series": "5.0.0-next.7" }, "engines": { "node": ">= 18" From 73de2cecdac382e5a643f971235e5934afb16600 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Tue, 25 Mar 2025 10:38:18 +0200 Subject: [PATCH 062/105] Release redis@5.0.0-next.7 --- packages/redis/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/redis/package.json b/packages/redis/package.json index b974a59ebf0..8b1567afb1d 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 0f24c7fc2127ac1fa442ac4d88d25ddab11f6ed6 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Tue, 25 Mar 2025 11:28:06 +0200 Subject: [PATCH 063/105] entraid: update readme.md (#2916) --- packages/entraid/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/README.md b/packages/entraid/README.md index 7a1ab1a94a2..83ddd1a3914 100644 --- a/packages/entraid/README.md +++ b/packages/entraid/README.md @@ -39,7 +39,7 @@ The first step to using @redis/entraid is choosing the right credentials provide ```typescript import { createClient } from '@redis/client'; -import { EntraIdCredentialsProviderFactory } from '@redis/entraid'; +import { EntraIdCredentialsProviderFactory } from '@redis/entraid/dist/lib'; const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({ clientId: 'your-client-id', From a7c96a01f8fc9000870e82587932f9480b63a4df Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Tue, 25 Mar 2025 13:50:16 +0200 Subject: [PATCH 064/105] fix (entraid): correct package entry point structure (#2917) - last time we forgot to include `index.ts` --- packages/entraid/README.md | 6 +++--- packages/entraid/tsconfig.json | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/entraid/README.md b/packages/entraid/README.md index 83ddd1a3914..10ecfec9973 100644 --- a/packages/entraid/README.md +++ b/packages/entraid/README.md @@ -39,7 +39,7 @@ The first step to using @redis/entraid is choosing the right credentials provide ```typescript import { createClient } from '@redis/client'; -import { EntraIdCredentialsProviderFactory } from '@redis/entraid/dist/lib'; +import { EntraIdCredentialsProviderFactory } from '@redis/entraid'; const provider = EntraIdCredentialsProviderFactory.createForClientCredentials({ clientId: 'your-client-id', @@ -86,7 +86,7 @@ const provider = EntraIdCredentialsProviderFactory.createForUserAssignedManagedI ### DefaultAzureCredential Authentication -tip: see a real sample here: [samples/interactive-browser/index.ts](./samples/interactive-browser/index.ts) +tip: see a real sample here: [samples/interactive-browser/index.ts](./samples/interactive-browser/index.ts) The DefaultAzureCredential from @azure/identity provides a simplified authentication experience that automatically tries different authentication methods based on the environment. This is especially useful for applications that need to work in different environments (local development, CI/CD, and production). @@ -128,7 +128,7 @@ When using the `createForDefaultAzureCredential` method, you need to: 2. Pass the same parameters to the factory method that you would use with the `getToken()` method: - `scopes`: The Redis scope (use the exported `REDIS_SCOPE_DEFAULT` constant) - `options`: Any additional options for the getToken method - + This factory method creates a wrapper around DefaultAzureCredential that adapts it to the Redis client's authentication system, while maintaining all the flexibility of the original Azure Identity authentication. diff --git a/packages/entraid/tsconfig.json b/packages/entraid/tsconfig.json index 414dc1fe755..47100f5b87d 100644 --- a/packages/entraid/tsconfig.json +++ b/packages/entraid/tsconfig.json @@ -4,7 +4,8 @@ "outDir": "./dist" }, "include": [ - "./lib/**/*.ts" + "./lib/**/*.ts", + "./index.ts" ], "exclude": [ "./lib/**/*.spec.ts", From 924dafabc3c177cb1d3be4df0d685f679b5bbc19 Mon Sep 17 00:00:00 2001 From: Svetlin Pavlov <58084028+spavlov6@users.noreply.github.com> Date: Wed, 26 Mar 2025 15:17:35 +0200 Subject: [PATCH 065/105] refactor(test-utils): remove TODO comments and TypeScript ignore directives for socket port (#2915) --- packages/test-utils/lib/index.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 1c564749ff2..117d089bcbe 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -250,8 +250,6 @@ export default class TestUtils { ...options.clientOptions, socket: { ...options.clientOptions?.socket, - // TODO - // @ts-ignore port: (await dockerPromise).port } }); @@ -325,8 +323,6 @@ export default class TestUtils { ...options.clientOptions, socket: { ...options.clientOptions?.socket, - // TODO - // @ts-ignore port: (await dockerPromise).port } }, options.poolOptions); From bdf95fdfca16408adc078726282ce8a43211c077 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Wed, 2 Apr 2025 14:48:21 +0100 Subject: [PATCH 066/105] fix: loosen @azure/identity constraint for @redis/entraid (#2920) --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index ad9cef61f4e..4eba7e0a788 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -17,7 +17,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "dependencies": { - "@azure/identity": "4.7.0", + "@azure/identity": "^4.7.0", "@azure/msal-node": "^2.16.1" }, "peerDependencies": { From 4d659f0b446d19b409f53eafbf7317f5fbb917a9 Mon Sep 17 00:00:00 2001 From: Daniel Hensby Date: Mon, 7 Apr 2025 13:29:13 +0100 Subject: [PATCH 067/105] docs: update the default credential provider example (#2919) --- packages/entraid/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/entraid/README.md b/packages/entraid/README.md index 10ecfec9973..733cf895a7e 100644 --- a/packages/entraid/README.md +++ b/packages/entraid/README.md @@ -18,8 +18,8 @@ Secure token-based authentication for Redis clients using Microsoft Entra ID (fo ```bash -npm install "@redis/client@5.0.0-next.6" -npm install "@redis/entraid@5.0.0-next.6" +npm install "@redis/client@5.0.0-next.7" +npm install "@redis/entraid@5.0.0-next.7" ``` ## Getting Started @@ -92,11 +92,11 @@ The DefaultAzureCredential from @azure/identity provides a simplified authentica ```typescript import { createClient } from '@redis/client'; -import { DefaultAzureCredential } from '@azure/identity'; -import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from '@redis/entraid/dist/lib/entra-id-credentials-provider-factory'; +import { getDefaultAzureCredential } from '@azure/identity'; +import { EntraIdCredentialsProviderFactory, REDIS_SCOPE_DEFAULT } from '@redis/entraid'; // Create a DefaultAzureCredential instance -const credential = new DefaultAzureCredential(); +const credential = getDefaultAzureCredential(); // Create a provider using DefaultAzureCredential const provider = EntraIdCredentialsProviderFactory.createForDefaultAzureCredential({ From 5295926cc0604ca7185566f05fa9fb28848253db Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Tue, 29 Apr 2025 10:56:38 +0300 Subject: [PATCH 068/105] bump test container to 8.0-RC2-pre (#2927) --- .github/workflows/tests.yml | 2 +- packages/search/lib/commands/INFO.spec.ts | 95 +---------------------- 2 files changed, 2 insertions(+), 95 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 366a24b4dc6..4ad7883a262 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -22,7 +22,7 @@ jobs: fail-fast: false matrix: node-version: [ '18', '20', '22' ] - redis-version: [ 'rs-7.2.0-v13', 'rs-7.4.0-v1', '8.0-M05-pre' ] + redis-version: [ 'rs-7.2.0-v13', 'rs-7.4.0-v1', '8.0-RC2-pre' ] steps: - uses: actions/checkout@v4 with: diff --git a/packages/search/lib/commands/INFO.spec.ts b/packages/search/lib/commands/INFO.spec.ts index caf311e07e9..b52e99ab9b0 100644 --- a/packages/search/lib/commands/INFO.spec.ts +++ b/packages/search/lib/commands/INFO.spec.ts @@ -18,100 +18,7 @@ describe('INFO', () => { field: SCHEMA_FIELD_TYPE.TEXT }); const ret = await client.ft.info('index'); - // effectively testing that stopwords_list is not in ret - assert.deepEqual( - ret, - { - index_name: 'index', - index_options: [], - index_definition: Object.create(null, { - - indexes_all: { - value: 'false', - configurable: true, - enumerable: true - }, - - default_score: { - value: '1', - configurable: true, - enumerable: true - }, - key_type: { - value: 'HASH', - configurable: true, - enumerable: true - }, - prefixes: { - value: [''], - configurable: true, - enumerable: true - } - }), - attributes: [Object.create(null, { - identifier: { - value: 'field', - configurable: true, - enumerable: true - }, - attribute: { - value: 'field', - configurable: true, - enumerable: true - }, - type: { - value: 'TEXT', - configurable: true, - enumerable: true - }, - WEIGHT: { - value: '1', - configurable: true, - enumerable: true - } - })], - num_docs: 0, - max_doc_id: 0, - num_terms: 0, - num_records: 0, - inverted_sz_mb: 0, - vector_index_sz_mb: 0, - total_inverted_index_blocks: 0, - offset_vectors_sz_mb: 0, - doc_table_size_mb: 0, - sortable_values_size_mb: 0, - key_table_size_mb: 0, - records_per_doc_avg: NaN, - bytes_per_record_avg: NaN, - cleaning: 0, - offsets_per_term_avg: NaN, - offset_bits_per_record_avg: NaN, - geoshapes_sz_mb: 0, - hash_indexing_failures: 0, - indexing: 0, - percent_indexed: 1, - number_of_uses: 1, - tag_overhead_sz_mb: 0, - text_overhead_sz_mb: 0, - total_index_memory_sz_mb: 0, - total_indexing_time: 0, - gc_stats: { - bytes_collected: 0, - total_ms_run: 0, - total_cycles: 0, - average_cycle_time_ms: NaN, - last_run_time_ms: 0, - gc_numeric_trees_missed: 0, - gc_blocks_denied: 0 - }, - cursor_stats: { - global_idle: 0, - global_total: 0, - index_capacity: 128, - index_total: 0 - }, - } - ); + assert.equal(ret.index_name, 'index'); }, GLOBAL.SERVERS.OPEN); From 048df302e40a9ced034b9a40d7dcfced511e15bb Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 14:38:32 +0300 Subject: [PATCH 069/105] Fix imports (#2929) * fix: exports align exports with v4 as much as possible * document breaking changes * export type return SetOptions export --- docs/v4-to-v5.md | 9 +++++++++ packages/client/index.ts | 8 ++++---- packages/redis/index.ts | 10 +++++----- packages/search/lib/index.ts | 26 ++++++++++++++++++++------ packages/time-series/lib/index.ts | 1 + 5 files changed, 39 insertions(+), 15 deletions(-) diff --git a/docs/v4-to-v5.md b/docs/v4-to-v5.md index 57ad3f9bbf6..3b09658b66f 100644 --- a/docs/v4-to-v5.md +++ b/docs/v4-to-v5.md @@ -141,6 +141,8 @@ In older versions, if the socket disconnects during the pipeline execution, i.e. In v5, any unwritten commands (in the same pipeline) will be discarded. +- `RedisFlushModes` -> `REDIS_FLUSH_MODES` [^enum-to-constants] + ## Commands ### Redis @@ -221,6 +223,13 @@ In v5, any unwritten commands (in the same pipeline) will be discarded. - `FT.SUGDEL`: [^boolean-to-number] - `FT.CURSOR READ`: `cursor` type changed from `number` to `string` (in and out) to avoid issues when the number is bigger than `Number.MAX_SAFE_INTEGER`. See [here](https://github.com/redis/node-redis/issues/2561). +- `AggregateGroupByReducers` -> `FT_AGGREGATE_GROUP_BY_REDUCERS` [^enum-to-constants] +- `AggregateSteps` -> `FT_AGGREGATE_STEPS` [^enum-to-constants] +- `RedisSearchLanguages` -> `REDISEARCH_LANGUAGE` [^enum-to-constants] +- `SchemaFieldTypes` -> `SCHEMA_FIELD_TYPE` [^enum-to-constants] +- `SchemaTextFieldPhonetics` -> `SCHEMA_TEXT_FIELD_PHONETIC` [^enum-to-constants] +- `SearchOptions` -> `FtSearchOptions` +- `VectorAlgorithms` -> `SCHEMA_VECTOR_FIELD_ALGORITHM` [^enum-to-constants] ### Time Series diff --git a/packages/client/index.ts b/packages/client/index.ts index 56cdf703ca3..e426badf126 100644 --- a/packages/client/index.ts +++ b/packages/client/index.ts @@ -2,7 +2,7 @@ export { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping/* export { RESP_TYPES } from './lib/RESP/decoder'; export { VerbatimString } from './lib/RESP/verbatim-string'; export { defineScript } from './lib/lua-script'; -// export * from './lib/errors'; +export * from './lib/errors'; import RedisClient, { RedisClientOptions, RedisClientType } from './lib/client'; export { RedisClientOptions, RedisClientType }; @@ -20,8 +20,8 @@ import RedisSentinel from './lib/sentinel'; export { RedisSentinelOptions, RedisSentinelType } from './lib/sentinel/types'; export const createSentinel = RedisSentinel.create; -// export { GeoReplyWith } from './lib/commands/generic-transformers'; +export { GEO_REPLY_WITH, GeoReplyWith } from './lib/commands/GEOSEARCH_WITH'; -// export { SetOptions } from './lib/commands/SET'; +export { SetOptions } from './lib/commands/SET'; -// export { RedisFlushModes } from './lib/commands/FLUSHALL'; +export { REDIS_FLUSH_MODES } from './lib/commands/FLUSHALL'; diff --git a/packages/redis/index.ts b/packages/redis/index.ts index 73477363d3b..61da052ea20 100644 --- a/packages/redis/index.ts +++ b/packages/redis/index.ts @@ -19,11 +19,11 @@ import RedisJSON from '@redis/json'; import RediSearch from '@redis/search'; import RedisTimeSeries from '@redis/time-series'; -// export * from '@redis/client'; -// export * from '@redis/bloom'; -// export * from '@redis/json'; -// export * from '@redis/search'; -// export * from '@redis/time-series'; +export * from '@redis/client'; +export * from '@redis/bloom'; +export * from '@redis/json'; +export * from '@redis/search'; +export * from '@redis/time-series'; const modules = { ...RedisBloomModules, diff --git a/packages/search/lib/index.ts b/packages/search/lib/index.ts index 34d57e8ae5e..9bcfb91b956 100644 --- a/packages/search/lib/index.ts +++ b/packages/search/lib/index.ts @@ -1,7 +1,21 @@ -export { default } from './commands'; +export { default } from './commands' -export { SCHEMA_FIELD_TYPE, SchemaFieldType } from './commands/CREATE'; - -// export { RediSearchSchema, RedisSearchLanguages, SchemaFieldTypes, SchemaTextFieldPhonetics, SearchReply, VectorAlgorithms } from './commands'; -// export { AggregateGroupByReducers, AggregateSteps } from './commands/AGGREGATE'; -// export { SearchOptions } from './commands/SEARCH'; +export { SearchReply } from './commands/SEARCH' +export { RediSearchSchema } from './commands/CREATE' +export { + REDISEARCH_LANGUAGE, + RediSearchLanguage, + SCHEMA_FIELD_TYPE, + SchemaFieldType, + SCHEMA_TEXT_FIELD_PHONETIC, + SchemaTextFieldPhonetic, + SCHEMA_VECTOR_FIELD_ALGORITHM, + SchemaVectorFieldAlgorithm +} from './commands/CREATE' +export { + FT_AGGREGATE_GROUP_BY_REDUCERS, + FtAggregateGroupByReducer, + FT_AGGREGATE_STEPS, + FtAggregateStep +} from './commands/AGGREGATE' +export { FtSearchOptions } from './commands/SEARCH' diff --git a/packages/time-series/lib/index.ts b/packages/time-series/lib/index.ts index bd0be1e9cea..52422bf1b5a 100644 --- a/packages/time-series/lib/index.ts +++ b/packages/time-series/lib/index.ts @@ -5,3 +5,4 @@ export { } from './commands'; export { TIME_SERIES_AGGREGATION_TYPE, TimeSeriesAggregationType } from './commands/CREATERULE'; export { TIME_SERIES_BUCKET_TIMESTAMP, TimeSeriesBucketTimestamp } from './commands/RANGE'; +export { TIME_SERIES_REDUCERS, TimeSeriesReducer } from './commands/MRANGE_GROUPBY'; From 10ff6debab44591f1104bc5e1500e79d64fac505 Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Wed, 30 Apr 2025 15:56:29 +0300 Subject: [PATCH 070/105] fix(sentinel): Migrated to the new testing framework, fixed issues that were discovered during transition * [CAE-342] Fix a couple of bugs * Fixed issue with nodes masterauth persistency, changed docker container * [CAE-342] Fixed a couple of sentinel issues, enabled most tests * [CAE-342] Added comment * [CAE-342] Migrate majority of tests to testUtils * [CAE-342] Minor refactor * . * [CAE-342] Using cae containers for sentinel * [CAE-342] Improved resiliency of the legacy tests, added TSdoc comment * [CAE-342] Some extra logging, removed unneeded changes * [CAE-342] Moved docker env as optional part of redisserverdockerconfig * [CAE-342] Move password to serverArguments * [CAE-342] Moved ts-node to devDependencies * [CAE-342] Reverted legacy testing framework improvements --- package-lock.json | 203 +- package.json | 3 +- packages/client/lib/client/index.ts | 17 + packages/client/lib/sentinel/index.spec.ts | 2023 +++++++++----------- packages/client/lib/sentinel/index.ts | 16 +- packages/client/lib/sentinel/test-util.ts | 2 +- packages/client/lib/test-utils.ts | 83 +- packages/test-utils/lib/dockers.ts | 165 +- packages/test-utils/lib/index.ts | 124 +- 9 files changed, 1421 insertions(+), 1215 deletions(-) diff --git a/package-lock.json b/package-lock.json index 25a1dc9d51c..064b1158f50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "workspaces": [ "./packages/*" ], + "dependencies": { + "ts-node": "^10.9.2" + }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/mocha": "^10.0.6", @@ -664,6 +667,28 @@ "node": ">=6.9.0" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.19.12", "cpu": [ @@ -804,7 +829,6 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -820,7 +844,6 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -1067,10 +1090,6 @@ "resolved": "packages/entraid", "link": true }, - "node_modules/@redis/graph": { - "resolved": "packages/graph", - "link": true - }, "node_modules/@redis/json": { "resolved": "packages/json", "link": true @@ -1164,6 +1183,30 @@ "dev": true, "license": "MIT" }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -1247,7 +1290,6 @@ }, "node_modules/@types/node": { "version": "20.11.16", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -1330,6 +1372,30 @@ "node": ">= 0.6" } }, + "node_modules/acorn": { + "version": "8.14.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", + "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.0", "license": "MIT", @@ -1448,6 +1514,12 @@ "dev": true, "license": "MIT" }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "dev": true, @@ -2315,6 +2387,12 @@ } } }, + "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==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.3", "dev": true, @@ -5088,6 +5166,12 @@ "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==", + "license": "ISC" + }, "node_modules/marked": { "version": "4.3.0", "dev": true, @@ -7576,6 +7660,58 @@ "node": ">=0.8.0" } }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@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", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "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==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/tslib": { "version": "2.6.2", "license": "0BSD" @@ -7741,7 +7877,6 @@ }, "node_modules/typescript": { "version": "5.3.3", - "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7780,7 +7915,6 @@ }, "node_modules/undici-types": { "version": "5.26.5", - "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -7956,6 +8090,12 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "license": "MIT" + }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -8324,6 +8464,15 @@ "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==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "dev": true, @@ -8353,7 +8502,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8362,12 +8511,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" } }, "packages/client": { "name": "@redis/client", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -8383,7 +8532,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "dependencies": { "@azure/identity": "4.7.0", @@ -8402,7 +8551,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" } }, "packages/entraid/node_modules/@types/node": { @@ -8425,6 +8574,7 @@ "packages/graph": { "name": "@redis/graph", "version": "5.0.0-next.6", + "extraneous": true, "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8438,7 +8588,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8447,19 +8597,18 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" } }, "packages/redis": { - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "dependencies": { - "@redis/bloom": "5.0.0-next.6", - "@redis/client": "5.0.0-next.6", - "@redis/graph": "5.0.0-next.6", - "@redis/json": "5.0.0-next.6", - "@redis/search": "5.0.0-next.6", - "@redis/time-series": "5.0.0-next.6" + "@redis/bloom": "5.0.0-next.7", + "@redis/client": "5.0.0-next.7", + "@redis/json": "5.0.0-next.7", + "@redis/search": "5.0.0-next.7", + "@redis/time-series": "5.0.0-next.7" }, "engines": { "node": ">= 18" @@ -8467,7 +8616,7 @@ }, "packages/search": { "name": "@redis/search", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8476,7 +8625,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" } }, "packages/test-utils": { @@ -8545,7 +8694,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.0.0-next.6", + "version": "5.0.0-next.7", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8554,7 +8703,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.6" + "@redis/client": "^5.0.0-next.7" } } } diff --git a/package.json b/package.json index 0a29c71f831..2ff2d4825ae 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "release-it": "^17.0.3", "tsx": "^4.7.0", "typedoc": "^0.25.7", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "ts-node": "^10.9.2" } } diff --git a/packages/client/lib/client/index.ts b/packages/client/lib/client/index.ts index 5dae1271ecb..f48e03d0c19 100644 --- a/packages/client/lib/client/index.ts +++ b/packages/client/lib/client/index.ts @@ -299,6 +299,9 @@ export default class RedisClient< #monitorCallback?: MonitorCallback; private _self = this; private _commandOptions?: CommandOptions; + // flag used to annotate that the client + // was in a watch transaction when + // a topology change occured #dirtyWatch?: string; #epoch: number; #watchEpoch?: number; @@ -325,6 +328,20 @@ export default class RedisClient< return this._self.#watchEpoch !== undefined; } + /** + * Indicates whether the client's WATCH command has been invalidated by a topology change. + * When this returns true, any transaction using WATCH will fail with a WatchError. + * @returns true if the watched keys have been modified, false otherwise + */ + get isDirtyWatch(): boolean { + return this._self.#dirtyWatch !== undefined + } + + /** + * Marks the client's WATCH command as invalidated due to a topology change. + * This will cause any subsequent EXEC in a transaction to fail with a WatchError. + * @param msg - The error message explaining why the WATCH is dirty + */ setDirtyWatch(msg: string) { this._self.#dirtyWatch = msg; } diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index be5522bdd8d..567da4b1a50 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -1,29 +1,339 @@ import { strict as assert } from 'node:assert'; import { setTimeout } from 'node:timers/promises'; +import testUtils, { GLOBAL, MATH_FUNCTION } from '../test-utils'; +import { RESP_TYPES } from '../RESP/decoder'; import { WatchError } from "../errors"; import { RedisSentinelConfig, SentinelFramework } from "./test-util"; -import { RedisNode, RedisSentinelClientType, RedisSentinelEvent, RedisSentinelType } from "./types"; -import { RedisSentinelFactory } from '.'; -import { RedisClientType } from '../client'; +import { RedisSentinelEvent, RedisSentinelType, RedisSentinelClientType, RedisNode } from "./types"; import { RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping, NumberReply } from '../RESP/types'; - import { promisify } from 'node:util'; import { exec } from 'node:child_process'; -import { RESP_TYPES } from '../RESP/decoder'; -import { defineScript } from '../lua-script'; -import { MATH_FUNCTION } from '../commands/FUNCTION_LOAD.spec'; -import RedisBloomModules from '@redis/bloom'; -import { RedisTcpSocketOptions } from '../client/socket'; -import { SQUARE_SCRIPT } from '../client/index.spec'; - const execAsync = promisify(exec); -/* used to ensure test environment resets to normal state - i.e. - - all redis nodes are active and are part of the topology - before allowing things to continue. -*/ +[GLOBAL.SENTINEL.OPEN, GLOBAL.SENTINEL.PASSWORD].forEach(testOptions => { + const passIndex = testOptions.serverArguments.indexOf('--requirepass')+1; + let password: string | undefined = undefined; + if (passIndex != 0) { + password = testOptions.serverArguments[passIndex]; + } + + describe(`test with password - ${password}`, () => { + testUtils.testWithClientSentinel('client should be authenticated', async sentinel => { + await assert.doesNotReject(sentinel.set('x', 1)); + }, testOptions); + + testUtils.testWithClientSentinel('try to connect multiple times', async sentinel => { + await assert.rejects(sentinel.connect()); + }, testOptions); + + + testUtils.testWithClientSentinel('should respect type mapping', async sentinel => { + const typeMapped = sentinel.withTypeMapping({ + [RESP_TYPES.SIMPLE_STRING]: Buffer + }); + + const resp = await typeMapped.ping(); + assert.deepEqual(resp, Buffer.from('PONG')); + }, testOptions); + + testUtils.testWithClientSentinel('many readers', async sentinel => { + await sentinel.set("x", 1); + for (let i = 0; i < 10; i++) { + if (await sentinel.get("x") == "1") { + break; + } + await setTimeout(1000); + } + + const promises: Array> = []; + for (let i = 0; i < 500; i++) { + promises.push(sentinel.get("x")); + } + + const resp = await Promise.all(promises); + assert.equal(resp.length, 500); + for (let i = 0; i < 500; i++) { + assert.equal(resp[i], "1", `failed on match at ${i}`); + } + }, testOptions); + + testUtils.testWithClientSentinel('use', async sentinel => { + await sentinel.use( + async (client: any ) => { + await assert.doesNotReject(client.get('x')); + } + ); + }, testOptions); + + testUtils.testWithClientSentinel('watch does not carry over leases', async sentinel => { + assert.equal(await sentinel.use(client => client.watch("x")), 'OK') + assert.equal(await sentinel.use(client => client.set('x', 1)), 'OK'); + assert.deepEqual(await sentinel.use(client => client.multi().get('x').exec()), ['1']); + }, testOptions); + + testUtils.testWithClientSentinel('plain pubsub - channel', async sentinel => { + let pubSubResolve; + const pubSubPromise = new Promise((res) => { + pubSubResolve = res; + }); + + let tester = false; + await sentinel.subscribe('test', () => { + tester = true; + pubSubResolve(1); + }) + + await sentinel.publish('test', 'hello world'); + await pubSubPromise; + assert.equal(tester, true); + + // now unsubscribe + tester = false; + await sentinel.unsubscribe('test') + await sentinel.publish('test', 'hello world'); + await setTimeout(1000); + + assert.equal(tester, false); + }, testOptions); + + testUtils.testWithClientSentinel('plain pubsub - pattern', async sentinel => { + let pubSubResolve; + const pubSubPromise = new Promise((res) => { + pubSubResolve = res; + }); + + let tester = false; + await sentinel.pSubscribe('test*', () => { + tester = true; + pubSubResolve(1); + }) + + await sentinel.publish('testy', 'hello world'); + await pubSubPromise; + assert.equal(tester, true); + + // now unsubscribe + tester = false; + await sentinel.pUnsubscribe('test*'); + await sentinel.publish('testy', 'hello world'); + await setTimeout(1000); + + assert.equal(tester, false); + }, testOptions) + }); +}); + +describe(`test with scripts`, () => { + testUtils.testWithClientSentinel('with script', async sentinel => { + const [, reply] = await Promise.all([ + sentinel.set('key', '2'), + sentinel.square('key') + ]); + + assert.equal(reply, 4); + }, GLOBAL.SENTINEL.WITH_SCRIPT); + + testUtils.testWithClientSentinel('with script multi', async sentinel => { + const reply = await sentinel.multi().set('key', 2).square('key').exec(); + assert.deepEqual(reply, ['OK', 4]); + }, GLOBAL.SENTINEL.WITH_SCRIPT); + + testUtils.testWithClientSentinel('use with script', async sentinel => { + const reply = await sentinel.use( + async (client: any) => { + assert.equal(await client.set('key', '2'), 'OK'); + assert.equal(await client.get('key'), '2'); + return client.square('key') + } + ); + }, GLOBAL.SENTINEL.WITH_SCRIPT) +}); + + +describe(`test with functions`, () => { + testUtils.testWithClientSentinel('with function', async sentinel => { + await sentinel.functionLoad( + MATH_FUNCTION.code, + { REPLACE: true } + ); + + await sentinel.set('key', '2'); + const resp = await sentinel.math.square('key'); + + assert.equal(resp, 4); + }, GLOBAL.SENTINEL.WITH_FUNCTION); + + testUtils.testWithClientSentinel('with function multi', async sentinel => { + await sentinel.functionLoad( + MATH_FUNCTION.code, + { REPLACE: true } + ); + + const reply = await sentinel.multi().set('key', 2).math.square('key').exec(); + assert.deepEqual(reply, ['OK', 4]); + }, GLOBAL.SENTINEL.WITH_FUNCTION); + + testUtils.testWithClientSentinel('use with function', async sentinel => { + await sentinel.functionLoad( + MATH_FUNCTION.code, + { REPLACE: true } + ); + + const reply = await sentinel.use( + async (client: any) => { + await client.set('key', '2'); + return client.math.square('key'); + } + ); + + assert.equal(reply, 4); + }, GLOBAL.SENTINEL.WITH_FUNCTION); +}); + +describe(`test with modules`, () => { + testUtils.testWithClientSentinel('with module', async sentinel => { + const resp = await sentinel.bf.add('key', 'item') + assert.equal(resp, true); + }, GLOBAL.SENTINEL.WITH_MODULE); + + testUtils.testWithClientSentinel('with module multi', async sentinel => { + const resp = await sentinel.multi().bf.add('key', 'item').exec(); + assert.deepEqual(resp, [true]); + }, GLOBAL.SENTINEL.WITH_MODULE); + + testUtils.testWithClientSentinel('use with module', async sentinel => { + const reply = await sentinel.use( + async (client: any) => { + return client.bf.add('key', 'item'); + } + ); + + assert.equal(reply, true); + }, GLOBAL.SENTINEL.WITH_MODULE); +}); + +describe(`test with replica pool size 1`, () => { + testUtils.testWithClientSentinel('client lease', async sentinel => { + sentinel.on("error", () => { }); + + const clientLease = await sentinel.aquire(); + clientLease.set('x', 456); + + let matched = false; + /* waits for replication */ + for (let i = 0; i < 15; i++) { + try { + assert.equal(await sentinel.get("x"), '456'); + matched = true; + break; + } catch (err) { + await setTimeout(1000); + } + } + + clientLease.release(); + + assert.equal(matched, true); + }, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1); + + testUtils.testWithClientSentinel('block on pool', async sentinel => { + const promise = sentinel.use( + async client => { + await setTimeout(1000); + return await client.get("x"); + } + ) + + await sentinel.set("x", 1); + assert.equal(await promise, null); + }, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1); + + testUtils.testWithClientSentinel('pipeline', async sentinel => { + const resp = await sentinel.multi().set('x', 1).get('x').execAsPipeline(); + assert.deepEqual(resp, ['OK', '1']); + }, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1); +}); + +describe(`test with masterPoolSize 2, reserve client true`, () => { + // TODO: flaky test, sometimes fails with `promise1 === null` + testUtils.testWithClientSentinel('reserve client, takes a client out of pool', async sentinel => { + const promise1 = sentinel.use( + async client => { + const val = await client.get("x"); + await client.set("x", 2); + return val; + } + ) + + const promise2 = sentinel.use( + async client => { + return client.get("x"); + } + ) + + await sentinel.set("x", 1); + assert.equal(await promise1, "1"); + assert.equal(await promise2, "2"); + }, Object.assign(GLOBAL.SENTINEL.WITH_RESERVE_CLIENT_MASTER_POOL_SIZE_2, {skipTest: true})); +}); + +describe(`test with masterPoolSize 2`, () => { + testUtils.testWithClientSentinel('multple clients', async sentinel => { + sentinel.on("error", () => { }); + + const promise = sentinel.use( + async client => { + await sentinel!.set("x", 1); + await client.get("x"); + } + ) + + await assert.doesNotReject(promise); + }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); + + testUtils.testWithClientSentinel('use - watch - clean', async sentinel => { + let promise = sentinel.use(async (client) => { + await client.set("x", 1); + await client.watch("x"); + return client.multi().get("x").exec(); + }); + + assert.deepEqual(await promise, ['1']); + }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); + + testUtils.testWithClientSentinel('use - watch - dirty', async sentinel => { + let promise = sentinel.use(async (client) => { + await client.set('x', 1); + await client.watch('x'); + await sentinel!.set('x', 2); + return client.multi().get('x').exec(); + }); + + await assert.rejects(promise, new WatchError()); + }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); + + testUtils.testWithClientSentinel('lease - watch - clean', async sentinel => { + const leasedClient = await sentinel.aquire(); + await leasedClient.set('x', 1); + await leasedClient.watch('x'); + assert.deepEqual(await leasedClient.multi().get('x').exec(), ['1']) + }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); + + testUtils.testWithClientSentinel('lease - watch - dirty', async sentinel => { + const leasedClient = await sentinel.aquire(); + await leasedClient.set('x', 1); + await leasedClient.watch('x'); + await leasedClient.set('x', 2); + + await assert.rejects(leasedClient.multi().get('x').exec(), new WatchError()); + }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); +}); + +// TODO: Figure out how to modify the test utils +// so it would have fine grained controll over +// sentinel +// it should somehow replicate the `SentinelFramework` object functionallities async function steadyState(frame: SentinelFramework) { let checkedMaster = false; let checkedReplicas = false; @@ -34,7 +344,6 @@ async function steadyState(frame: SentinelFramework) { checkedMaster = true; } } - if (!checkedReplicas) { const replicas = (await frame.sentinelReplicas()); checkedReplicas = true; @@ -43,17 +352,14 @@ async function steadyState(frame: SentinelFramework) { } } } - let nodeResolve, nodeReject; const nodePromise = new Promise((res, rej) => { nodeResolve = res; nodeReject = rej; }) - const seenNodes = new Set(); let sentinel: RedisSentinelType | undefined; const tracer = []; - try { sentinel = frame.getSentinelClient({ replicaPoolSize: 1, scanInterval: 2000 }, false) .on('topology-change', (event: RedisSentinelEvent) => { @@ -66,7 +372,6 @@ async function steadyState(frame: SentinelFramework) { }).on('error', err => { }); sentinel.setTracer(tracer); await sentinel.connect(); - await nodePromise; await sentinel.flushAll(); @@ -77,1189 +382,593 @@ async function steadyState(frame: SentinelFramework) { } } -["redis-sentinel-test-password", undefined].forEach(function (password) { - describe.skip(`Sentinel - password = ${password}`, () => { - const config: RedisSentinelConfig = { sentinelName: "test", numberOfNodes: 3, password: password }; - const frame = new SentinelFramework(config); - let tracer = new Array(); - let stopMeasuringBlocking = false; - let longestDelta = 0; - let longestTestDelta = 0; - let last: number; - - before(async function () { - this.timeout(15000); - - last = Date.now(); - - function deltaMeasurer() { - const delta = Date.now() - last; - if (delta > longestDelta) { - longestDelta = delta; - } - if (delta > longestTestDelta) { - longestTestDelta = delta; +describe.skip('legacy tests', () => { + const config: RedisSentinelConfig = { sentinelName: "test", numberOfNodes: 3, password: undefined }; + const frame = new SentinelFramework(config); + let tracer = new Array(); + let stopMeasuringBlocking = false; + let longestDelta = 0; + let longestTestDelta = 0; + let last: number; + + before(async function () { + this.timeout(15000); + + last = Date.now(); + + function deltaMeasurer() { + const delta = Date.now() - last; + if (delta > longestDelta) { + longestDelta = delta; + } + if (delta > longestTestDelta) { + longestTestDelta = delta; + } + if (!stopMeasuringBlocking) { + last = Date.now(); + setImmediate(deltaMeasurer); + } + } + setImmediate(deltaMeasurer); + await frame.spawnRedisSentinel(); + }); + + after(async function () { + this.timeout(15000); + + stopMeasuringBlocking = true; + + await frame.cleanup(); + }) + + describe('Sentinel Client', function () { + let sentinel: RedisSentinelType | undefined; + + beforeEach(async function () { + this.timeout(0); + + await frame.getAllRunning(); + await steadyState(frame); + longestTestDelta = 0; + }) + + afterEach(async function () { + this.timeout(60000); + // avoid errors in afterEach that end testing + if (sentinel !== undefined) { + sentinel.on('error', () => { }); + } + + if (this!.currentTest!.state === 'failed') { + console.log(`longest event loop blocked delta: ${longestDelta}`); + console.log(`longest event loop blocked in failing test: ${longestTestDelta}`); + console.log("trace:"); + for (const line of tracer) { + console.log(line); } - if (!stopMeasuringBlocking) { - last = Date.now(); - setImmediate(deltaMeasurer); + console.log(`sentinel object state:`) + console.log(`master: ${JSON.stringify(sentinel?.getMasterNode())}`) + console.log(`replicas: ${JSON.stringify(sentinel?.getReplicaNodes().entries)}`) + const results = await Promise.all([ + frame.sentinelSentinels(), + frame.sentinelMaster(), + frame.sentinelReplicas() + ]) + + console.log(`sentinel sentinels:\n${JSON.stringify(results[0], undefined, '\t')}`); + console.log(`sentinel master:\n${JSON.stringify(results[1], undefined, '\t')}`); + console.log(`sentinel replicas:\n${JSON.stringify(results[2], undefined, '\t')}`); + const { stdout, stderr } = await execAsync("docker ps -a"); + console.log(`docker stdout:\n${stdout}`); + const ids = frame.getAllDockerIds(); + console.log("docker logs"); + for (const [id, port] of ids) { + console.log(`${id}/${port}\n`); + const { stdout, stderr } = await execAsync(`docker logs ${id}`, {maxBuffer: 8192 * 8192 * 4}); + console.log(stdout); } } + tracer.length = 0; + + if (sentinel !== undefined) { + await sentinel.destroy(); + sentinel = undefined; + } + }) + + it('use', async function () { + this.timeout(60000); - setImmediate(deltaMeasurer); + sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); + sentinel.on("error", () => { }); + await sentinel.connect(); - await frame.spawnRedisSentinel(); + await sentinel.use( + async (client: RedisSentinelClientType, ) => { + const masterNode = sentinel!.getMasterNode(); + await frame.stopNode(masterNode!.port.toString()); + await assert.doesNotReject(client.get('x')); + } + ); }); - - after(async function () { - this.timeout(15000); - - stopMeasuringBlocking = true; - - await frame.cleanup(); - }) - - describe('Sentinel Client', function () { - let sentinel: RedisSentinelType< RedisModules, RedisFunctions, RedisScripts, RespVersions, TypeMapping> | undefined; - - beforeEach(async function () { - this.timeout(0); - - await frame.getAllRunning(); - await steadyState(frame); - longestTestDelta = 0; + // stops master to force sentinel to update + it('stop master', async function () { + this.timeout(60000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + + tracer.push(`connected`); + + let masterChangeResolve; + const masterChangePromise = new Promise((res) => { + masterChangeResolve = res; }) - - afterEach(async function () { - this.timeout(30000); - // avoid errors in afterEach that end testing - if (sentinel !== undefined) { - sentinel.on('error', () => { }); - } - - if (this!.currentTest!.state === 'failed') { - console.log(`longest event loop blocked delta: ${longestDelta}`); - console.log(`longest event loop blocked in failing test: ${longestTestDelta}`); - console.log("trace:"); - for (const line of tracer) { - console.log(line); - } - console.log(`sentinel object state:`) - console.log(`master: ${JSON.stringify(sentinel?.getMasterNode())}`) - console.log(`replicas: ${JSON.stringify(sentinel?.getReplicaNodes().entries)}`) - const results = await Promise.all([ - frame.sentinelSentinels(), - frame.sentinelMaster(), - frame.sentinelReplicas() - ]) - console.log(`sentinel sentinels:\n${JSON.stringify(results[0], undefined, '\t')}`); - console.log(`sentinel master:\n${JSON.stringify(results[1], undefined, '\t')}`); - console.log(`sentinel replicas:\n${JSON.stringify(results[2], undefined, '\t')}`); - const { stdout, stderr } = await execAsync("docker ps -a"); - console.log(`docker stdout:\n${stdout}`); - - const ids = frame.getAllDockerIds(); - console.log("docker logs"); - - for (const [id, port] of ids) { - console.log(`${id}/${port}\n`); - const { stdout, stderr } = await execAsync(`docker logs ${id}`, {maxBuffer: 8192 * 8192 * 4}); - console.log(stdout); - } - } - tracer.length = 0; - - if (sentinel !== undefined) { - await sentinel.destroy(); - sentinel = undefined; + const masterNode = await sentinel.getMasterNode(); + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push(`got expected master change event`); + masterChangeResolve(event.node); } + }); + + tracer.push(`stopping master node`); + await frame.stopNode(masterNode!.port.toString()); + tracer.push(`stopped master node`); + + tracer.push(`waiting on master change promise`); + const newMaster = await masterChangePromise as RedisNode; + tracer.push(`got new master node of ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); + }); + + // if master changes, client should make sure user knows watches are invalid + it('watch across master change', async function () { + this.timeout(60000); + + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + + tracer.push("connected"); + + const client = await sentinel.aquire(); + tracer.push("aquired lease"); + + await client.set("x", 1); + await client.watch("x"); + + tracer.push("did a watch on lease"); + + let resolve; + const promise = new Promise((res) => { + resolve = res; }) - - it('basic bootstrap', async function () { - sentinel = frame.getSentinelClient(); - await sentinel.connect(); - await assert.doesNotReject(sentinel.set('x', 1)); + const masterNode = sentinel.getMasterNode(); + tracer.push(`got masterPort as ${masterNode!.port}`); - }); - - it('basic teardown worked', async function () { - const nodePorts = frame.getAllNodesPort(); - const sentinelPorts = frame.getAllSentinelsPort(); - - assert.notEqual(nodePorts.length, 0); - assert.notEqual(sentinelPorts.length, 0); - - sentinel = frame.getSentinelClient(); - await sentinel.connect(); - - await assert.doesNotReject(sentinel.get('x')); - }); - - it('try to connect multiple times', async function () { - sentinel = frame.getSentinelClient(); - const connectPromise = sentinel.connect(); - await assert.rejects(sentinel.connect()); - await connectPromise; - }); - - it('with type mapping', async function () { - const commandOptions = { - typeMapping: { - [RESP_TYPES.SIMPLE_STRING]: Buffer - } + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push("resolving promise"); + resolve(event.node); } - sentinel = frame.getSentinelClient({ commandOptions: commandOptions }); - await sentinel.connect(); - - const resp = await sentinel.ping(); - assert.deepEqual(resp, Buffer.from('PONG')) + }); + + tracer.push("stopping master node"); + await frame.stopNode(masterNode!.port.toString()); + tracer.push("stopped master node and waiting on promise"); + + const newMaster = await promise as RedisNode; + tracer.push(`promise returned, newMaster = ${JSON.stringify(newMaster)}`); + assert.notEqual(masterNode!.port, newMaster.port); + tracer.push(`newMaster does not equal old master`); + + tracer.push(`waiting to assert that a multi/exec now fails`); + await assert.rejects(async () => { await client.multi().get("x").exec() }, new Error("sentinel config changed in middle of a WATCH Transaction")); + tracer.push(`asserted that a multi/exec now fails`); + }); + + // same as above, but set a watch before and after master change, shouldn't change the fact that watches are invalid + it('watch before and after master change', async function () { + this.timeout(60000); + + sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + tracer.push("connected"); + + const client = await sentinel.aquire(); + tracer.push("got leased client"); + await client.set("x", 1); + await client.watch("x"); + + tracer.push("set and watched x"); + + let resolve; + const promise = new Promise((res) => { + resolve = res; }) - - it('with a script', async function () { - const options = { - scripts: { - square: SQUARE_SCRIPT - } + + const masterNode = sentinel.getMasterNode(); + tracer.push(`initial masterPort = ${masterNode!.port} `); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push("got a master change event that is not the same as before"); + resolve(event.node); } - - sentinel = frame.getSentinelClient(options); - await sentinel.connect(); - - const [, reply] = await Promise.all([ - sentinel.set('key', '2'), - sentinel.square('key') - ]); - - assert.equal(reply, 4); + }); + + tracer.push("stopping master"); + await frame.stopNode(masterNode!.port.toString()); + tracer.push("stopped master"); + + tracer.push("waiting on master change promise"); + const newMaster = await promise as RedisNode; + tracer.push(`got master change port as ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); + + tracer.push("watching again, shouldn't matter"); + await client.watch("y"); + + tracer.push("expecting multi to be rejected"); + await assert.rejects(async () => { await client.multi().get("x").exec() }, new Error("sentinel config changed in middle of a WATCH Transaction")); + tracer.push("multi was rejected"); + }); + + + // pubsub continues to work, even with a master change + it('pubsub - channel - with master change', async function () { + this.timeout(60000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + tracer.push(`connected`); + + let pubSubResolve; + const pubSubPromise = new Promise((res) => { + pubSubResolve = res; }) - it('multi with a script', async function () { - const options = { - scripts: { - square: SQUARE_SCRIPT - } + let tester = false; + await sentinel.subscribe('test', () => { + tracer.push(`got pubsub message`); + tester = true; + pubSubResolve(1); + }) + + let masterChangeResolve; + const masterChangePromise = new Promise((res) => { + masterChangeResolve = res; + }) + + const masterNode = sentinel.getMasterNode(); + tracer.push(`got masterPort as ${masterNode!.port}`); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push("got a master change event that is not the same as before"); + masterChangeResolve(event.node); } - - sentinel = frame.getSentinelClient(options); - await sentinel.connect(); + }); + + tracer.push("stopping master"); + await frame.stopNode(masterNode!.port.toString()); + tracer.push("stopped master and waiting on change promise"); + + const newMaster = await masterChangePromise as RedisNode; + tracer.push(`got master change port as ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); + + tracer.push(`publishing pubsub message`); + await sentinel.publish('test', 'hello world'); + tracer.push(`published pubsub message and waiting pn pubsub promise`); + await pubSubPromise; + tracer.push(`got pubsub promise`); + + assert.equal(tester, true); + + // now unsubscribe + tester = false + await sentinel.unsubscribe('test') + await sentinel.publish('test', 'hello world'); + await setTimeout(1000); + + assert.equal(tester, false); + }); - const reply = await sentinel.multi().set('key', 2).square('key').exec(); + it('pubsub - pattern - with master change', async function () { + this.timeout(60000); - assert.deepEqual(reply, ['OK', 4]); + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + tracer.push(`connected`); + + let pubSubResolve; + const pubSubPromise = new Promise((res) => { + pubSubResolve = res; }) - - it('with a function', async function () { - const options = { - functions: { - math: MATH_FUNCTION.library - } - } - sentinel = frame.getSentinelClient(options); - await sentinel.connect(); - - await sentinel.functionLoad( - MATH_FUNCTION.code, - { REPLACE: true } - ); - - await sentinel.set('key', '2'); - const resp = await sentinel.math.square('key'); - - assert.equal(resp, 4); + + let tester = false; + await sentinel.pSubscribe('test*', () => { + tracer.push(`got pubsub message`); + tester = true; + pubSubResolve(1); }) - it('multi with a function', async function () { - const options = { - functions: { - math: MATH_FUNCTION.library - } + let masterChangeResolve; + const masterChangePromise = new Promise((res) => { + masterChangeResolve = res; + }) + + const masterNode = sentinel.getMasterNode(); + tracer.push(`got masterPort as ${masterNode!.port}`); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + tracer.push("got a master change event that is not the same as before"); + masterChangeResolve(event.node); } - sentinel = frame.getSentinelClient(options); - await sentinel.connect(); - - await sentinel.functionLoad( - MATH_FUNCTION.code, - { REPLACE: true } - ); + }); + + tracer.push("stopping master"); + await frame.stopNode(masterNode!.port.toString()); + tracer.push("stopped master and waiting on master change promise"); + + const newMaster = await masterChangePromise as RedisNode; + tracer.push(`got master change port as ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); - const reply = await sentinel.multi().set('key', 2).math.square('key').exec(); - assert.deepEqual(reply, ['OK', 4]); + tracer.push(`publishing pubsub message`); + await sentinel.publish('testy', 'hello world'); + tracer.push(`published pubsub message and waiting on pubsub promise`); + await pubSubPromise; + tracer.push(`got pubsub promise`); + assert.equal(tester, true); + + // now unsubscribe + tester = false + await sentinel.pUnsubscribe('test*'); + await sentinel.publish('testy', 'hello world'); + await setTimeout(1000); + + assert.equal(tester, false); + }); + + // if we stop a node, the comand should "retry" until we reconfigure topology and execute on new topology + it('command immeaditely after stopping master', async function () { + this.timeout(60000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + + tracer.push("connected"); + + let masterChangeResolve; + const masterChangePromise = new Promise((res) => { + masterChangeResolve = res; }) - - it('with a module', async function () { - const options = { - modules: RedisBloomModules + + const masterNode = sentinel.getMasterNode(); + tracer.push(`original master port = ${masterNode!.port}`); + + let changeCount = 0; + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { + changeCount++; + tracer.push(`got topology-change event we expected`); + masterChangeResolve(event.node); } - sentinel = frame.getSentinelClient(options); - await sentinel.connect(); - - const resp = await sentinel.bf.add('key', 'item') - assert.equal(resp, true); + }); + + tracer.push(`stopping masterNode`); + await frame.stopNode(masterNode!.port.toString()); + tracer.push(`stopped masterNode`); + assert.equal(await sentinel.set('x', 123), 'OK'); + tracer.push(`did the set operation`); + const presumamblyNewMaster = sentinel.getMasterNode(); + tracer.push(`new master node seems to be ${presumamblyNewMaster?.port} and waiting on master change promise`); + + const newMaster = await masterChangePromise as RedisNode; + tracer.push(`got new masternode event saying master is at ${newMaster.port}`); + assert.notEqual(masterNode!.port, newMaster.port); + + tracer.push(`doing the get`); + const val = await sentinel.get('x'); + tracer.push(`did the get and got ${val}`); + const newestMaster = sentinel.getMasterNode() + tracer.push(`after get, we see master as ${newestMaster?.port}`); + + switch (changeCount) { + case 1: + // if we only changed masters once, we should have the proper value + assert.equal(val, '123'); + break; + case 2: + // we changed masters twice quickly, so probably didn't replicate + // therefore, this is soewhat flakey, but the above is the common case + assert(val == '123' || val == null); + break; + default: + assert(false, "unexpected case"); + } + }); + + it('shutdown sentinel node', async function () { + this.timeout(60000); + + sentinel = frame.getSentinelClient(); + sentinel.setTracer(tracer); + sentinel.on("error", () => { }); + await sentinel.connect(); + tracer.push("connected"); + + let sentinelChangeResolve; + const sentinelChangePromise = new Promise((res) => { + sentinelChangeResolve = res; }) - it('multi with a module', async function () { - const options = { - modules: RedisBloomModules + const sentinelNode = sentinel.getSentinelNode(); + tracer.push(`sentinelNode = ${sentinelNode?.port}`) + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "SENTINEL_CHANGE") { + tracer.push("got sentinel change event"); + sentinelChangeResolve(event.node); } - sentinel = frame.getSentinelClient(options); - await sentinel.connect(); - - const resp = await sentinel.multi().bf.add('key', 'item').exec(); - assert.deepEqual(resp, [true]); + }); + + tracer.push("Stopping sentinel node"); + await frame.stopSentinel(sentinelNode!.port.toString()); + tracer.push("Stopped sentinel node and waiting on sentinel change promise"); + const newSentinel = await sentinelChangePromise as RedisNode; + tracer.push("got sentinel change promise"); + assert.notEqual(sentinelNode!.port, newSentinel.port); + }); + + it('timer works, and updates sentinel list', async function () { + this.timeout(60000); + + sentinel = frame.getSentinelClient({ scanInterval: 1000 }); + sentinel.setTracer(tracer); + await sentinel.connect(); + tracer.push("connected"); + + let sentinelChangeResolve; + const sentinelChangePromise = new Promise((res) => { + sentinelChangeResolve = res; }) - - it('many readers', async function () { - this.timeout(10000); - - sentinel = frame.getSentinelClient({ replicaPoolSize: 8 }); - await sentinel.connect(); - - await sentinel.set("x", 1); - for (let i = 0; i < 10; i++) { - if (await sentinel.get("x") == "1") { - break; - } - await setTimeout(1000); - } - - const promises: Array> = []; - for (let i = 0; i < 500; i++) { - promises.push(sentinel.get("x")); - } - - const resp = await Promise.all(promises); - assert.equal(resp.length, 500); - for (let i = 0; i < 500; i++) { - assert.equal(resp[i], "1", `failed on match at ${i}`); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "SENTINE_LIST_CHANGE" && event.size == 4) { + tracer.push(`got sentinel list change event with right size`); + sentinelChangeResolve(event.size); } }); - - it('use', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); - sentinel.on("error", () => { }); - await sentinel.connect(); - - await sentinel.use( - async (client: RedisSentinelClientType, ) => { - const masterNode = sentinel!.getMasterNode(); - await frame.stopNode(masterNode!.port.toString()); - await assert.doesNotReject(client.get('x')); - } - ); - }); - - it('use with script', async function () { - this.timeout(10000); - - const options = { - scripts: { - square: SQUARE_SCRIPT - } - } - - sentinel = frame.getSentinelClient(options); - await sentinel.connect(); - - const reply = await sentinel.use( - async (client: RedisSentinelClientType) => { - assert.equal(await client.set('key', '2'), 'OK'); - assert.equal(await client.get('key'), '2'); - return client.square('key') - } - ); - - assert.equal(reply, 4); - }) - - it('use with a function', async function () { - this.timeout(10000); - - const options = { - functions: { - math: MATH_FUNCTION.library - } - } - sentinel = frame.getSentinelClient(options); - await sentinel.connect(); - - await sentinel.functionLoad( - MATH_FUNCTION.code, - { REPLACE: true } - ); - - const reply = await sentinel.use( - async (client: RedisSentinelClientType) => { - await client.set('key', '2'); - return client.math.square('key'); - } - ); - - assert.equal(reply, 4); - }) - - it('use with a module', async function () { - const options = { - modules: RedisBloomModules - } - sentinel = frame.getSentinelClient(options); - await sentinel.connect(); - - const reply = await sentinel.use( - async (client: RedisSentinelClientType) => { - return client.bf.add('key', 'item'); - } - ); - - assert.equal(reply, true); - }) - - it('block on pool', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); - sentinel.on("error", () => { }); - await sentinel.connect(); - - const promise = sentinel.use( - async client => { - await setTimeout(1000); - return await client.get("x"); - } - ) - - await sentinel.set("x", 1); - assert.equal(await promise, null); - }); - it('reserve client, takes a client out of pool', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ masterPoolSize: 2, reserveClient: true }); - await sentinel.connect(); - - const promise1 = sentinel.use( - async client => { - const val = await client.get("x"); - await client.set("x", 2); - return val; - } - ) + tracer.push(`adding sentinel`); + await frame.addSentinel(); + tracer.push(`added sentinel and waiting on sentinel change promise`); + const newSentinelSize = await sentinelChangePromise as number; - const promise2 = sentinel.use( - async client => { - return client.get("x"); - } - ) + assert.equal(newSentinelSize, 4); + }); - await sentinel.set("x", 1); - assert.equal(await promise1, "1"); - assert.equal(await promise2, "2"); + it('stop replica, bring back replica', async function () { + this.timeout(60000); + + sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); + sentinel.setTracer(tracer); + sentinel.on('error', err => { }); + await sentinel.connect(); + tracer.push("connected"); + + let sentinelRemoveResolve; + const sentinelRemovePromise = new Promise((res) => { + sentinelRemoveResolve = res; }) - - it('multiple clients', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); - sentinel.on("error", () => { }); - await sentinel.connect(); - - let set = false; - - const promise = sentinel.use( - async client => { - await sentinel!.set("x", 1); - await client.get("x"); - } - ) - - await assert.doesNotReject(promise); - }); - - // by taking a lease, we know we will block on master as no clients are available, but as read occuring, means replica read occurs - it('replica reads', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); - sentinel.on("error", () => { }); - await sentinel.connect(); - - const clientLease = await sentinel.aquire(); - clientLease.set('x', 456); - - let matched = false; - /* waits for replication */ - for (let i = 0; i < 15; i++) { - try { - assert.equal(await sentinel.get("x"), '456'); - matched = true; - break; - } catch (err) { - await setTimeout(1000); + + const replicaPort = await frame.getRandonNonMasterNode(); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "REPLICA_REMOVE") { + if (event.node.port.toString() == replicaPort) { + tracer.push("got expected replica removed event"); + sentinelRemoveResolve(event.node); + } else { + tracer.push(`got replica removed event for a different node: ${event.node.port}`); } } - - clientLease.release(); - - assert.equal(matched, true); }); - - it('pipeline', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); - await sentinel.connect(); - - const resp = await sentinel.multi().set('x', 1).get('x').execAsPipeline(); - - assert.deepEqual(resp, ['OK', '1']); + + tracer.push(`replicaPort = ${replicaPort} and stopping it`); + await frame.stopNode(replicaPort); + tracer.push("stopped replica and waiting on sentinel removed promise"); + const stoppedNode = await sentinelRemovePromise as RedisNode; + tracer.push("got removed promise"); + assert.equal(stoppedNode.port, Number(replicaPort)); + + let sentinelRestartedResolve; + const sentinelRestartedPromise = new Promise((res) => { + sentinelRestartedResolve = res; }) - - it('use - watch - clean', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); - await sentinel.connect(); - - let promise = sentinel.use(async (client) => { - await client.set("x", 1); - await client.watch("x"); - return client.multi().get("x").exec(); - }); - - assert.deepEqual(await promise, ['1']); - }); - - it('use - watch - dirty', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); - await sentinel.connect(); - - let promise = sentinel.use(async (client) => { - await client.set('x', 1); - await client.watch('x'); - await sentinel!.set('x', 2); - return client.multi().get('x').exec(); - }); - - await assert.rejects(promise, new WatchError()); - }); - - it('lease - watch - clean', async function () { - sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); - await sentinel.connect(); - - const leasedClient = await sentinel.aquire(); - await leasedClient.set('x', 1); - await leasedClient.watch('x'); - assert.deepEqual(await leasedClient.multi().get('x').exec(), ['1']) - }); - - it('lease - watch - dirty', async function () { - sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); - await sentinel.connect(); - - const leasedClient = await sentinel.aquire(); - await leasedClient.set('x', 1); - await leasedClient.watch('x'); - await leasedClient.set('x', 2); - - await assert.rejects(leasedClient.multi().get('x').exec(), new WatchError()); - }); - - - it('watch does not carry through leases', async function () { - this.timeout(10000); - sentinel = frame.getSentinelClient(); - await sentinel.connect(); - - // each of these commands is an independent lease - assert.equal(await sentinel.use(client => client.watch("x")), 'OK') - assert.equal(await sentinel.use(client => client.set('x', 1)), 'OK'); - assert.deepEqual(await sentinel.use(client => client.multi().get('x').exec()), ['1']); - }); - - // stops master to force sentinel to update - it('stop master', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient(); - sentinel.setTracer(tracer); - sentinel.on("error", () => { }); - await sentinel.connect(); - - tracer.push(`connected`); - - let masterChangeResolve; - const masterChangePromise = new Promise((res) => { - masterChangeResolve = res; - }) - - const masterNode = await sentinel.getMasterNode(); - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { - tracer.push(`got expected master change event`); - masterChangeResolve(event.node); - } - }); - - tracer.push(`stopping master node`); - await frame.stopNode(masterNode!.port.toString()); - tracer.push(`stopped master node`); - - tracer.push(`waiting on master change promise`); - const newMaster = await masterChangePromise as RedisNode; - tracer.push(`got new master node of ${newMaster.port}`); - assert.notEqual(masterNode!.port, newMaster.port); - }); - - // if master changes, client should make sure user knows watches are invalid - it('watch across master change', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); - sentinel.setTracer(tracer); - sentinel.on("error", () => { }); - await sentinel.connect(); - - tracer.push("connected"); - - const client = await sentinel.aquire(); - tracer.push("aquired lease"); - - await client.set("x", 1); - await client.watch("x"); - - tracer.push("did a watch on lease"); - - let resolve; - const promise = new Promise((res) => { - resolve = res; - }) - - const masterNode = sentinel.getMasterNode(); - tracer.push(`got masterPort as ${masterNode!.port}`); - - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { - tracer.push("resolving promise"); - resolve(event.node); - } - }); - - tracer.push("stopping master node"); - await frame.stopNode(masterNode!.port.toString()); - tracer.push("stopped master node and waiting on promise"); - - const newMaster = await promise as RedisNode; - tracer.push(`promise returned, newMaster = ${JSON.stringify(newMaster)}`); - assert.notEqual(masterNode!.port, newMaster.port); - tracer.push(`newMaster does not equal old master`); - - tracer.push(`waiting to assert that a multi/exec now fails`); - await assert.rejects(async () => { await client.multi().get("x").exec() }, new Error("sentinel config changed in middle of a WATCH Transaction")); - tracer.push(`asserted that a multi/exec now fails`); - }); - - // same as above, but set a watch before and after master change, shouldn't change the fact that watches are invalid - it('watch before and after master change', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ masterPoolSize: 2 }); - sentinel.setTracer(tracer); - sentinel.on("error", () => { }); - await sentinel.connect(); - tracer.push("connected"); - - const client = await sentinel.aquire(); - tracer.push("got leased client"); - await client.set("x", 1); - await client.watch("x"); - - tracer.push("set and watched x"); - - let resolve; - const promise = new Promise((res) => { - resolve = res; - }) - - const masterNode = sentinel.getMasterNode(); - tracer.push(`initial masterPort = ${masterNode!.port} `); - - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { - tracer.push("got a master change event that is not the same as before"); - resolve(event.node); - } - }); - - tracer.push("stopping master"); - await frame.stopNode(masterNode!.port.toString()); - tracer.push("stopped master"); - - tracer.push("waiting on master change promise"); - const newMaster = await promise as RedisNode; - tracer.push(`got master change port as ${newMaster.port}`); - assert.notEqual(masterNode!.port, newMaster.port); - - tracer.push("watching again, shouldn't matter"); - await client.watch("y"); - - tracer.push("expecting multi to be rejected"); - await assert.rejects(async () => { await client.multi().get("x").exec() }, new Error("sentinel config changed in middle of a WATCH Transaction")); - tracer.push("multi was rejected"); - }); - - it('plain pubsub - channel', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient(); - sentinel.setTracer(tracer); - await sentinel.connect(); - tracer.push(`connected`); - - let pubSubResolve; - const pubSubPromise = new Promise((res) => { - pubSubResolve = res; - }); - - let tester = false; - await sentinel.subscribe('test', () => { - tracer.push(`got pubsub message`); - tester = true; - pubSubResolve(1); - }) - - tracer.push(`publishing pubsub message`); - await sentinel.publish('test', 'hello world'); - tracer.push(`waiting on pubsub promise`); - await pubSubPromise; - tracer.push(`got pubsub promise`); - assert.equal(tester, true); - - // now unsubscribe - tester = false - tracer.push(`unsubscribing pubsub listener`); - await sentinel.unsubscribe('test') - tracer.push(`pubishing pubsub message`); - await sentinel.publish('test', 'hello world'); - await setTimeout(1000); - - tracer.push(`ensuring pubsub was unsubscribed via an assert`); - assert.equal(tester, false); - }); - - it('plain pubsub - pattern', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient(); - sentinel.setTracer(tracer); - await sentinel.connect(); - tracer.push(`connected`); - - let pubSubResolve; - const pubSubPromise = new Promise((res) => { - pubSubResolve = res; - }); - - let tester = false; - await sentinel.pSubscribe('test*', () => { - tracer.push(`got pubsub message`); - tester = true; - pubSubResolve(1); - }) - - tracer.push(`publishing pubsub message`); - await sentinel.publish('testy', 'hello world'); - tracer.push(`waiting on pubsub promise`); - await pubSubPromise; - tracer.push(`got pubsub promise`); - assert.equal(tester, true); - - // now unsubscribe - tester = false - tracer.push(`unsubscribing pubsub listener`); - await sentinel.pUnsubscribe('test*'); - tracer.push(`pubishing pubsub message`); - await sentinel.publish('testy', 'hello world'); - await setTimeout(1000); - - tracer.push(`ensuring pubsub was unsubscribed via an assert`); - assert.equal(tester, false); - }); - - // pubsub continues to work, even with a master change - it('pubsub - channel - with master change', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient(); - sentinel.setTracer(tracer); - sentinel.on("error", () => { }); - await sentinel.connect(); - tracer.push(`connected`); - - let pubSubResolve; - const pubSubPromise = new Promise((res) => { - pubSubResolve = res; - }) - - let tester = false; - await sentinel.subscribe('test', () => { - tracer.push(`got pubsub message`); - tester = true; - pubSubResolve(1); - }) - - let masterChangeResolve; - const masterChangePromise = new Promise((res) => { - masterChangeResolve = res; - }) - - const masterNode = sentinel.getMasterNode(); - tracer.push(`got masterPort as ${masterNode!.port}`); - - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { - tracer.push("got a master change event that is not the same as before"); - masterChangeResolve(event.node); - } - }); - - tracer.push("stopping master"); - await frame.stopNode(masterNode!.port.toString()); - tracer.push("stopped master and waiting on change promise"); - - const newMaster = await masterChangePromise as RedisNode; - tracer.push(`got master change port as ${newMaster.port}`); - assert.notEqual(masterNode!.port, newMaster.port); - - tracer.push(`publishing pubsub message`); - await sentinel.publish('test', 'hello world'); - tracer.push(`published pubsub message and waiting pn pubsub promise`); - await pubSubPromise; - tracer.push(`got pubsub promise`); - - assert.equal(tester, true); - - // now unsubscribe - tester = false - await sentinel.unsubscribe('test') - await sentinel.publish('test', 'hello world'); - await setTimeout(1000); - - assert.equal(tester, false); - }); - - it('pubsub - pattern - with master change', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient(); - sentinel.setTracer(tracer); - sentinel.on("error", () => { }); - await sentinel.connect(); - tracer.push(`connected`); - - let pubSubResolve; - const pubSubPromise = new Promise((res) => { - pubSubResolve = res; - }) - - let tester = false; - await sentinel.pSubscribe('test*', () => { - tracer.push(`got pubsub message`); - tester = true; - pubSubResolve(1); - }) - - let masterChangeResolve; - const masterChangePromise = new Promise((res) => { - masterChangeResolve = res; - }) - - const masterNode = sentinel.getMasterNode(); - tracer.push(`got masterPort as ${masterNode!.port}`); - - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { - tracer.push("got a master change event that is not the same as before"); - masterChangeResolve(event.node); - } - }); - - tracer.push("stopping master"); - await frame.stopNode(masterNode!.port.toString()); - tracer.push("stopped master and waiting on master change promise"); - - const newMaster = await masterChangePromise as RedisNode; - tracer.push(`got master change port as ${newMaster.port}`); - assert.notEqual(masterNode!.port, newMaster.port); - - tracer.push(`publishing pubsub message`); - await sentinel.publish('testy', 'hello world'); - tracer.push(`published pubsub message and waiting on pubsub promise`); - await pubSubPromise; - tracer.push(`got pubsub promise`); - assert.equal(tester, true); - - // now unsubscribe - tester = false - await sentinel.pUnsubscribe('test*'); - await sentinel.publish('testy', 'hello world'); - await setTimeout(1000); - - assert.equal(tester, false); - }); - - // if we stop a node, the comand should "retry" until we reconfigure topology and execute on new topology - it('command immeaditely after stopping master', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient(); - sentinel.setTracer(tracer); - sentinel.on("error", () => { }); - await sentinel.connect(); - - tracer.push("connected"); - - let masterChangeResolve; - const masterChangePromise = new Promise((res) => { - masterChangeResolve = res; - }) - - const masterNode = sentinel.getMasterNode(); - tracer.push(`original master port = ${masterNode!.port}`); - - let changeCount = 0; - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "MASTER_CHANGE" && event.node.port != masterNode!.port) { - changeCount++; - tracer.push(`got topology-change event we expected`); - masterChangeResolve(event.node); - } - }); - - tracer.push(`stopping masterNode`); - await frame.stopNode(masterNode!.port.toString()); - tracer.push(`stopped masterNode`); - assert.equal(await sentinel.set('x', 123), 'OK'); - tracer.push(`did the set operation`); - const presumamblyNewMaster = sentinel.getMasterNode(); - tracer.push(`new master node seems to be ${presumamblyNewMaster?.port} and waiting on master change promise`); - - const newMaster = await masterChangePromise as RedisNode; - tracer.push(`got new masternode event saying master is at ${newMaster.port}`); - assert.notEqual(masterNode!.port, newMaster.port); - - tracer.push(`doing the get`); - const val = await sentinel.get('x'); - tracer.push(`did the get and got ${val}`); - const newestMaster = sentinel.getMasterNode() - tracer.push(`after get, we see master as ${newestMaster?.port}`); - - switch (changeCount) { - case 1: - // if we only changed masters once, we should have the proper value - assert.equal(val, '123'); - break; - case 2: - // we changed masters twice quickly, so probably didn't replicate - // therefore, this is soewhat flakey, but the above is the common case - assert(val == '123' || val == null); - break; - default: - assert(false, "unexpected case"); + + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "REPLICA_ADD") { + tracer.push("got replica added event"); + sentinelRestartedResolve(event.node); } }); - - it('shutdown sentinel node', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient(); - sentinel.setTracer(tracer); - sentinel.on("error", () => { }); - await sentinel.connect(); - tracer.push("connected"); - - let sentinelChangeResolve; - const sentinelChangePromise = new Promise((res) => { - sentinelChangeResolve = res; - }) - - const sentinelNode = sentinel.getSentinelNode(); - tracer.push(`sentinelNode = ${sentinelNode?.port}`) - - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "SENTINEL_CHANGE") { - tracer.push("got sentinel change event"); - sentinelChangeResolve(event.node); - } - }); - - tracer.push("Stopping sentinel node"); - await frame.stopSentinel(sentinelNode!.port.toString()); - tracer.push("Stopped sentinel node and waiting on sentinel change promise"); - const newSentinel = await sentinelChangePromise as RedisNode; - tracer.push("got sentinel change promise"); - assert.notEqual(sentinelNode!.port, newSentinel.port); - }); - - it('timer works, and updates sentinel list', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ scanInterval: 1000 }); - sentinel.setTracer(tracer); - await sentinel.connect(); - tracer.push("connected"); - - let sentinelChangeResolve; - const sentinelChangePromise = new Promise((res) => { - sentinelChangeResolve = res; - }) - - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "SENTINE_LIST_CHANGE" && event.size == 4) { - tracer.push(`got sentinel list change event with right size`); - sentinelChangeResolve(event.size); - } - }); - - tracer.push(`adding sentinel`); - await frame.addSentinel(); - tracer.push(`added sentinel and waiting on sentinel change promise`); - const newSentinelSize = await sentinelChangePromise as number; - - assert.equal(newSentinelSize, 4); - }); - - it('stop replica, bring back replica', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ replicaPoolSize: 1 }); - sentinel.setTracer(tracer); - sentinel.on('error', err => { }); - await sentinel.connect(); - tracer.push("connected"); - - let sentinelRemoveResolve; - const sentinelRemovePromise = new Promise((res) => { - sentinelRemoveResolve = res; - }) - - const replicaPort = await frame.getRandonNonMasterNode(); - - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "REPLICA_REMOVE") { - if (event.node.port.toString() == replicaPort) { - tracer.push("got expected replica removed event"); - sentinelRemoveResolve(event.node); - } else { - tracer.push(`got replica removed event for a different node: ${event.node.port}`); - } - } - }); - - tracer.push(`replicaPort = ${replicaPort} and stopping it`); - await frame.stopNode(replicaPort); - tracer.push("stopped replica and waiting on sentinel removed promise"); - const stoppedNode = await sentinelRemovePromise as RedisNode; - tracer.push("got removed promise"); - assert.equal(stoppedNode.port, Number(replicaPort)); - - let sentinelRestartedResolve; - const sentinelRestartedPromise = new Promise((res) => { - sentinelRestartedResolve = res; - }) - - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "REPLICA_ADD") { - tracer.push("got replica added event"); - sentinelRestartedResolve(event.node); - } - }); - - tracer.push("restarting replica"); - await frame.restartNode(replicaPort); - tracer.push("restarted replica and waiting on restart promise"); - const restartedNode = await sentinelRestartedPromise as RedisNode; - tracer.push("got restarted promise"); - assert.equal(restartedNode.port, Number(replicaPort)); - }) - - it('add a node / new replica', async function () { - this.timeout(30000); - - sentinel = frame.getSentinelClient({ scanInterval: 2000, replicaPoolSize: 1 }); - sentinel.setTracer(tracer); - // need to handle errors, as the spawning a new docker node can cause existing connections to time out - sentinel.on('error', err => { }); - await sentinel.connect(); - tracer.push("connected"); - - let nodeAddedResolve: (value: RedisNode) => void; - const nodeAddedPromise = new Promise((res) => { - nodeAddedResolve = res as (value: RedisNode) => void; - }); - - const portSet = new Set(); - for (const port of frame.getAllNodesPort()) { - portSet.add(port); - } - - // "on" and not "once" as due to connection timeouts, can happen multiple times, and want right one - sentinel.on('topology-change', (event: RedisSentinelEvent) => { - tracer.push(`got topology-change event: ${JSON.stringify(event)}`); - if (event.type === "REPLICA_ADD") { - if (!portSet.has(event.node.port)) { - tracer.push("got expected replica added event"); - nodeAddedResolve(event.node); - } - } - }); - - tracer.push("adding node"); - await frame.addNode(); - tracer.push("added node and waiting on added promise"); - await nodeAddedPromise; - }) + + tracer.push("restarting replica"); + await frame.restartNode(replicaPort); + tracer.push("restarted replica and waiting on restart promise"); + const restartedNode = await sentinelRestartedPromise as RedisNode; + tracer.push("got restarted promise"); + assert.equal(restartedNode.port, Number(replicaPort)); }) - - describe('Sentinel Factory', function () { - let master: RedisClientType | undefined; - let replica: RedisClientType | undefined; - - beforeEach(async function () { - this.timeout(0); - await frame.getAllRunning(); - - await steadyState(frame); - longestTestDelta = 0; - }) - - afterEach(async function () { - if (this!.currentTest!.state === 'failed') { - console.log(`longest event loop blocked delta: ${longestDelta}`); - console.log(`longest event loop blocked in failing test: ${longestTestDelta}`); - console.log("trace:"); - for (const line of tracer) { - console.log(line); - } - const results = await Promise.all([ - frame.sentinelSentinels(), - frame.sentinelMaster(), - frame.sentinelReplicas() - ]) - console.log(`sentinel sentinels:\n${JSON.stringify(results[0], undefined, '\t')}`); - console.log(`sentinel master:\n${JSON.stringify(results[1], undefined, '\t')}`); - console.log(`sentinel replicas:\n${JSON.stringify(results[2], undefined, '\t')}`); - const { stdout, stderr } = await execAsync("docker ps -a"); - console.log(`docker stdout:\n${stdout}`); - console.log(`docker stderr:\n${stderr}`); - } - tracer.length = 0; - - if (master !== undefined) { - if (master.isOpen) { - master.destroy(); - } - master = undefined; - } - - if (replica !== undefined) { - if (replica.isOpen) { - replica.destroy(); + it('add a node / new replica', async function () { + this.timeout(60000); + + sentinel = frame.getSentinelClient({ scanInterval: 2000, replicaPoolSize: 1 }); + sentinel.setTracer(tracer); + // need to handle errors, as the spawning a new docker node can cause existing connections to time out + sentinel.on('error', err => { }); + await sentinel.connect(); + tracer.push("connected"); + + let nodeAddedResolve: (value: RedisNode) => void; + const nodeAddedPromise = new Promise((res) => { + nodeAddedResolve = res as (value: RedisNode) => void; + }); + + const portSet = new Set(); + for (const port of frame.getAllNodesPort()) { + portSet.add(port); + } + + // "on" and not "once" as due to connection timeouts, can happen multiple times, and want right one + sentinel.on('topology-change', (event: RedisSentinelEvent) => { + tracer.push(`got topology-change event: ${JSON.stringify(event)}`); + if (event.type === "REPLICA_ADD") { + if (!portSet.has(event.node.port)) { + tracer.push("got expected replica added event"); + nodeAddedResolve(event.node); } - replica = undefined; } - }) - - it('sentinel factory - master', async function () { - const sentinelPorts = frame.getAllSentinelsPort(); - const sentinels: Array = []; - for (const port of sentinelPorts) { - sentinels.push({ host: "localhost", port: port }); - } - - const factory = new RedisSentinelFactory({ name: frame.config.sentinelName, sentinelRootNodes: sentinels, sentinelClientOptions: {password: password}, nodeClientOptions: {password: password} }) - await factory.updateSentinelRootNodes(); - - master = await factory.getMasterClient(); - await master.connect(); - - assert.equal(await master.set("x", 1), 'OK'); - }) - - it('sentinel factory - replica', async function () { - const sentinelPorts = frame.getAllSentinelsPort(); - const sentinels: Array = []; - - for (const port of sentinelPorts) { - sentinels.push({ host: "localhost", port: port }); - } - - const factory = new RedisSentinelFactory({ name: frame.config.sentinelName, sentinelRootNodes: sentinels, sentinelClientOptions: {password: password}, nodeClientOptions: {password: password} }) - await factory.updateSentinelRootNodes(); - - const masterNode = await factory.getMasterNode(); - replica = await factory.getReplicaClient(); - const replicaSocketOptions = replica.options?.socket as unknown as RedisTcpSocketOptions | undefined; - assert.notEqual(masterNode.port, replicaSocketOptions?.port) - }) - - it('sentinel factory - bad node', async function () { - const factory = new RedisSentinelFactory({ name: frame.config.sentinelName, sentinelRootNodes: [{ host: "locahost", port: 1 }] }); - await assert.rejects(factory.updateSentinelRootNodes(), new Error("Couldn't connect to any sentinel node")); - }) - - it('sentinel factory - invalid db name', async function () { - this.timeout(15000); - - const sentinelPorts = frame.getAllSentinelsPort(); - const sentinels: Array = []; - - for (const port of sentinelPorts) { - sentinels.push({ host: "localhost", port: port }); - } - - const factory = new RedisSentinelFactory({ name: "invalid-name", sentinelRootNodes: sentinels, sentinelClientOptions: {password: password}, nodeClientOptions: {password: password} }) - await assert.rejects(factory.updateSentinelRootNodes(), new Error("ERR No such master with that name")); - }) - - it('sentinel factory - no available nodes', async function () { - this.timeout(15000); - - const sentinelPorts = frame.getAllSentinelsPort(); - const sentinels: Array = []; - - for (const port of sentinelPorts) { - sentinels.push({ host: "localhost", port: port }); - } - - const factory = new RedisSentinelFactory({ name: frame.config.sentinelName, sentinelRootNodes: sentinels, sentinelClientOptions: {password: password}, nodeClientOptions: {password: password} }) - - for (const node of frame.getAllNodesPort()) { - await frame.stopNode(node.toString()); - } - - await setTimeout(1000); - - await assert.rejects(factory.getMasterNode(), new Error("Master Node Not Enumerated")); - }) + }); + + tracer.push("adding node"); + await frame.addNode(); + tracer.push("added node and waiting on added promise"); + await nodeAddedPromise; }) - }) + }); }); + + + diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index 92a87fbb145..73c4fffd070 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -345,9 +345,12 @@ export default class RedisSentinel< key: K, value: V ) { - const proxy = Object.create(this._self); - proxy._commandOptions = Object.create(this._self.#commandOptions ?? null); - proxy._commandOptions[key] = value; + const proxy = Object.create(this); + // Create new commandOptions object with the inherited properties + proxy._self.#commandOptions = { + ...(this._self.#commandOptions || {}), + [key]: value + }; return proxy as RedisSentinelType< M, F, @@ -682,9 +685,10 @@ class RedisSentinelInternal< async #connect() { let count = 0; - while (true) { + while (true) { this.#trace("starting connect loop"); + count+=1; if (this.#destroy) { this.#trace("in #connect and want to destroy") return; @@ -1109,7 +1113,7 @@ class RedisSentinelInternal< this.#trace(`transform: destroying old masters if open`); for (const client of this.#masterClients) { - masterWatches.push(client.isWatching); + masterWatches.push(client.isWatching || client.isDirtyWatch); if (client.isOpen) { client.destroy() @@ -1460,4 +1464,4 @@ export class RedisSentinelFactory extends EventEmitter { } }); } -} +} \ No newline at end of file diff --git a/packages/client/lib/sentinel/test-util.ts b/packages/client/lib/sentinel/test-util.ts index 25dd4c4371a..60696bc3437 100644 --- a/packages/client/lib/sentinel/test-util.ts +++ b/packages/client/lib/sentinel/test-util.ts @@ -602,4 +602,4 @@ export class SentinelFramework extends DockerBase { } } } -} +} \ No newline at end of file diff --git a/packages/client/lib/test-utils.ts b/packages/client/lib/test-utils.ts index f7862a9d685..63f43ba5e6a 100644 --- a/packages/client/lib/test-utils.ts +++ b/packages/client/lib/test-utils.ts @@ -2,9 +2,10 @@ import TestUtils from '@redis/test-utils'; import { SinonSpy } from 'sinon'; import { setTimeout } from 'node:timers/promises'; import { CredentialsProvider } from './authx'; -import { Command } from './RESP/types'; -import { BasicCommandParser } from './client/parser'; - +import { Command, NumberReply } from './RESP/types'; +import { BasicCommandParser, CommandParser } from './client/parser'; +import { defineScript } from './lua-script'; +import RedisBloomModules from '@redis/bloom'; const utils = TestUtils.createFromConfig({ dockerImageName: 'redislabs/client-libs-test', dockerImageVersionArgument: 'redis-version', @@ -42,6 +43,45 @@ const streamingCredentialsProvider: CredentialsProvider = } as const; +const SQUARE_SCRIPT = defineScript({ + SCRIPT: + `local number = redis.call('GET', KEYS[1]) + return number * number`, + NUMBER_OF_KEYS: 1, + FIRST_KEY_INDEX: 0, + parseCommand(parser: CommandParser, key: string) { + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => NumberReply +}); + +export const MATH_FUNCTION = { + name: 'math', + engine: 'LUA', + code: + `#!LUA name=math + redis.register_function { + function_name = "square", + callback = function(keys, args) + local number = redis.call('GET', keys[1]) + return number * number + end, + flags = { "no-writes" } + }`, + library: { + square: { + NAME: 'square', + IS_READ_ONLY: true, + NUMBER_OF_KEYS: 1, + FIRST_KEY_INDEX: 0, + parseCommand(parser: CommandParser, key: string) { + parser.pushKey(key); + }, + transformReply: undefined as unknown as () => NumberReply + } + } +}; + export const GLOBAL = { SERVERS: { OPEN: { @@ -86,6 +126,43 @@ export const GLOBAL = { useReplicas: true } } + }, + SENTINEL: { + OPEN: { + serverArguments: [...DEBUG_MODE_ARGS], + }, + PASSWORD: { + serverArguments: ['--requirepass', 'test_password', ...DEBUG_MODE_ARGS], + }, + WITH_SCRIPT: { + serverArguments: [...DEBUG_MODE_ARGS], + scripts: { + square: SQUARE_SCRIPT, + }, + }, + WITH_FUNCTION: { + serverArguments: [...DEBUG_MODE_ARGS], + functions: { + math: MATH_FUNCTION.library, + }, + }, + WITH_MODULE: { + serverArguments: [...DEBUG_MODE_ARGS], + modules: RedisBloomModules, + }, + WITH_REPLICA_POOL_SIZE_1: { + serverArguments: [...DEBUG_MODE_ARGS], + replicaPoolSize: 1, + }, + WITH_RESERVE_CLIENT_MASTER_POOL_SIZE_2: { + serverArguments: [...DEBUG_MODE_ARGS], + masterPoolSize: 2, + reserveClient: true, + }, + WITH_MASTER_POOL_SIZE_2: { + serverArguments: [...DEBUG_MODE_ARGS], + masterPoolSize: 2, + } } }; diff --git a/packages/test-utils/lib/dockers.ts b/packages/test-utils/lib/dockers.ts index e3ff5edc38b..3814a80923b 100644 --- a/packages/test-utils/lib/dockers.ts +++ b/packages/test-utils/lib/dockers.ts @@ -4,9 +4,11 @@ import { once } from 'node:events'; import { createClient } from '@redis/client/index'; import { setTimeout } from 'node:timers/promises'; // import { ClusterSlotsReply } from '@redis/client/dist/lib/commands/CLUSTER_SLOTS'; - import { execFile as execFileCallback } from 'node:child_process'; import { promisify } from 'node:util'; +import * as fs from 'node:fs'; +import * as os from 'node:os'; +import * as path from 'node:path'; const execAsync = promisify(execFileCallback); @@ -38,37 +40,64 @@ const portIterator = (async function* (): AsyncIterableIterator { throw new Error('All ports are in use'); })(); -export interface RedisServerDockerConfig { +interface RedisServerDockerConfig { image: string; version: string; } +interface SentinelConfig { + mode: "sentinel"; + mounts: Array; + port: number; +} + +interface ServerConfig { + mode: "server"; +} + +export type RedisServerDockerOptions = RedisServerDockerConfig & (SentinelConfig | ServerConfig) + export interface RedisServerDocker { port: number; dockerId: string; } -async function spawnRedisServerDocker({ - image, - version -}: RedisServerDockerConfig, serverArguments: Array): Promise { - const port = (await portIterator.next()).value; +async function spawnRedisServerDocker( +options: RedisServerDockerOptions, serverArguments: Array): Promise { + let port; + if (options.mode == "sentinel") { + port = options.port; + } else { + port = (await portIterator.next()).value; + } + const portStr = port.toString(); const dockerArgs = [ 'run', - '-e', `PORT=${portStr}`, + '--init', + '-e', `PORT=${portStr}` + ]; + + if (options.mode == "sentinel") { + options.mounts.forEach(mount => { + dockerArgs.push('-v', mount); + }); + } + + dockerArgs.push( '-d', '--network', 'host', - `${image}:${version}`, - '--port', portStr - ]; + `${options.image}:${options.version}` + ); if (serverArguments.length > 0) { - dockerArgs.push(...serverArguments); + for (let i = 0; i < serverArguments.length; i++) { + dockerArgs.push(serverArguments[i]) + } } - console.log(`[Docker] Spawning Redis container - Image: ${image}:${version}, Port: ${port}`); + console.log(`[Docker] Spawning Redis container - Image: ${options.image}:${options.version}, Port: ${port}, Mode: ${options.mode}`); const { stdout, stderr } = await execAsync('docker', dockerArgs); @@ -87,7 +116,7 @@ async function spawnRedisServerDocker({ } const RUNNING_SERVERS = new Map, ReturnType>(); -export function spawnRedisServer(dockerConfig: RedisServerDockerConfig, serverArguments: Array): Promise { +export function spawnRedisServer(dockerConfig: RedisServerDockerOptions, serverArguments: Array): Promise { const runningServer = RUNNING_SERVERS.get(serverArguments); if (runningServer) { return runningServer; @@ -113,7 +142,7 @@ after(() => { ); }); -export interface RedisClusterDockersConfig extends RedisServerDockerConfig { +export type RedisClusterDockersConfig = RedisServerDockerOptions & { numberOfMasters?: number; numberOfReplicas?: number; } @@ -178,7 +207,7 @@ async function spawnRedisClusterNodeDockers( } async function spawnRedisClusterNodeDocker( - dockersConfig: RedisClusterDockersConfig, + dockersConfig: RedisServerDockerOptions, serverArguments: Array, clientConfig?: Partial ) { @@ -291,3 +320,107 @@ after(() => { }) ); }); + + +const RUNNING_NODES = new Map, Array>(); +const RUNNING_SENTINELS = new Map, Array>(); + +export async function spawnRedisSentinel( + dockerConfigs: RedisServerDockerOptions, + serverArguments: Array, +): Promise> { + const runningNodes = RUNNING_SENTINELS.get(serverArguments); + if (runningNodes) { + return runningNodes; + } + + const passIndex = serverArguments.indexOf('--requirepass')+1; + let password: string | undefined = undefined; + if (passIndex != 0) { + password = serverArguments[passIndex]; + } + + const master = await spawnRedisServerDocker(dockerConfigs, serverArguments); + const redisNodes: Array = [master]; + const replicaPromises: Array> = []; + + const replicasCount = 2; + for (let i = 0; i < replicasCount; i++) { + replicaPromises.push((async () => { + const replica = await spawnRedisServerDocker(dockerConfigs, serverArguments); + const client = createClient({ + socket: { + port: replica.port + }, + password: password + }); + + await client.connect(); + await client.replicaOf("127.0.0.1", master.port); + await client.close(); + + return replica; + })()); + } + + const replicas = await Promise.all(replicaPromises); + redisNodes.push(...replicas); + RUNNING_NODES.set(serverArguments, redisNodes); + + const sentinelPromises: Array> = []; + const sentinelCount = 3; + + const appPrefix = 'sentinel-config-dir'; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), appPrefix)); + + for (let i = 0; i < sentinelCount; i++) { + sentinelPromises.push((async () => { + const port = (await portIterator.next()).value; + + let sentinelConfig = `port ${port} +sentinel monitor mymaster 127.0.0.1 ${master.port} 2 +sentinel down-after-milliseconds mymaster 5000 +sentinel failover-timeout mymaster 6000 +`; + if (password !== undefined) { + sentinelConfig += `requirepass ${password}\n`; + sentinelConfig += `sentinel auth-pass mymaster ${password}\n`; + } + + const dir = fs.mkdtempSync(path.join(tmpDir, i.toString())); + fs.writeFile(`${dir}/redis.conf`, sentinelConfig, err => { + if (err) { + console.error("failed to create temporary config file", err); + } + }); + + return await spawnRedisServerDocker( + { + image: dockerConfigs.image, + version: dockerConfigs.version, + mode: "sentinel", + mounts: [`${dir}/redis.conf:/redis/config/node-sentinel-1/redis.conf`], + port: port, + }, serverArguments); + })()); + } + + const sentinelNodes = await Promise.all(sentinelPromises); + RUNNING_SENTINELS.set(serverArguments, sentinelNodes); + + if (tmpDir) { + fs.rmSync(tmpDir, { recursive: true }); + } + + return sentinelNodes; +} + +after(() => { + return Promise.all( + [...RUNNING_NODES.values(), ...RUNNING_SENTINELS.values()].map(async dockersPromise => { + return Promise.all( + dockersPromise.map(({ dockerId }) => dockerRemove(dockerId)) + ); + }) + ); +}); diff --git a/packages/test-utils/lib/index.ts b/packages/test-utils/lib/index.ts index 117d089bcbe..8ed85bf6e3e 100644 --- a/packages/test-utils/lib/index.ts +++ b/packages/test-utils/lib/index.ts @@ -6,8 +6,11 @@ import { TypeMapping, // CommandPolicies, createClient, + createSentinel, RedisClientOptions, RedisClientType, + RedisSentinelOptions, + RedisSentinelType, RedisPoolOptions, RedisClientPoolType, createClientPool, @@ -15,7 +18,8 @@ import { RedisClusterOptions, RedisClusterType } from '@redis/client/index'; -import { RedisServerDockerConfig, spawnRedisServer, spawnRedisCluster } from './dockers'; +import { RedisNode } from '@redis/client/lib/sentinel/types' +import { spawnRedisServer, spawnRedisCluster, spawnRedisSentinel, RedisServerDockerOptions } from './dockers'; import yargs from 'yargs'; import { hideBin } from 'yargs/helpers'; @@ -68,6 +72,24 @@ interface ClientTestOptions< disableClientSetup?: boolean; } +interface SentinelTestOptions< + M extends RedisModules, + F extends RedisFunctions, + S extends RedisScripts, + RESP extends RespVersions, + TYPE_MAPPING extends TypeMapping +> extends CommonTestOptions { + sentinelOptions?: Partial>; + clientOptions?: Partial>; + scripts?: S; + functions?: F; + modules?: M; + disableClientSetup?: boolean; + replicaPoolSize?: number; + masterPoolSize?: number; + reserveClient?: boolean; +} + interface ClientPoolTestOptions< M extends RedisModules, F extends RedisFunctions, @@ -148,13 +170,14 @@ export default class TestUtils { } readonly #VERSION_NUMBERS: Array; - readonly #DOCKER_IMAGE: RedisServerDockerConfig; + readonly #DOCKER_IMAGE: RedisServerDockerOptions; constructor({ string, numbers }: Version, dockerImageName: string) { this.#VERSION_NUMBERS = numbers; this.#DOCKER_IMAGE = { image: dockerImageName, - version: string + version: string, + mode: "server" }; } @@ -179,7 +202,6 @@ export default class TestUtils { } isVersionGreaterThanHook(minimumVersion: Array | undefined): void { - const isVersionGreaterThanHook = this.isVersionGreaterThan.bind(this); const versionNumber = this.#VERSION_NUMBERS.join('.'); const minimumVersionString = minimumVersion?.join('.'); @@ -272,6 +294,81 @@ export default class TestUtils { }); } + testWithClientSentinel< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} + >( + title: string, + fn: (sentinel: RedisSentinelType) => unknown, + options: SentinelTestOptions + ): void { + let dockerPromises: ReturnType; + + const passIndex = options.serverArguments.indexOf('--requirepass')+1; + let password: string | undefined = undefined; + if (passIndex != 0) { + password = options.serverArguments[passIndex]; + } + + if (this.isVersionGreaterThan(options.minimumDockerVersion)) { + const dockerImage = this.#DOCKER_IMAGE; + before(function () { + this.timeout(30000); + dockerPromises = spawnRedisSentinel(dockerImage, options.serverArguments); + return dockerPromises; + }); + } + + it(title, async function () { + this.timeout(30000); + if (options.skipTest) return this.skip(); + if (!dockerPromises) return this.skip(); + + + const promises = await dockerPromises; + const rootNodes: Array = promises.map(promise => ({ + host: "127.0.0.1", + port: promise.port + })); + + const sentinel = createSentinel({ + name: 'mymaster', + sentinelRootNodes: rootNodes, + nodeClientOptions: { + password: password || undefined, + }, + sentinelClientOptions: { + password: password || undefined, + }, + replicaPoolSize: options?.replicaPoolSize || 0, + scripts: options?.scripts || {}, + modules: options?.modules || {}, + functions: options?.functions || {}, + masterPoolSize: options?.masterPoolSize || undefined, + reserveClient: options?.reserveClient || false, + }) as RedisSentinelType; + + if (options.disableClientSetup) { + return fn(sentinel); + } + + await sentinel.connect(); + + try { + await sentinel.flushAll(); + await fn(sentinel); + } finally { + if (sentinel.isOpen) { + await sentinel.flushAll(); + sentinel.destroy(); + } + } + }); + } + testWithClientIfVersionWithinRange< M extends RedisModules = {}, F extends RedisFunctions = {}, @@ -290,8 +387,27 @@ export default class TestUtils { } else { console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`) } + } + testWithClienSentineltIfVersionWithinRange< + M extends RedisModules = {}, + F extends RedisFunctions = {}, + S extends RedisScripts = {}, + RESP extends RespVersions = 2, + TYPE_MAPPING extends TypeMapping = {} +>( + range: ([minVersion: Array, maxVersion: Array] | [minVersion: Array, 'LATEST']), + title: string, + fn: (sentinel: RedisSentinelType) => unknown, + options: SentinelTestOptions +): void { + + if (this.isVersionInRange(range[0], range[1] === 'LATEST' ? [Infinity, Infinity, Infinity] : range[1])) { + return this.testWithClientSentinel(`${title} [${range[0].join('.')}] - [${(range[1] === 'LATEST') ? range[1] : range[1].join(".")}] `, fn, options) + } else { + console.warn(`Skipping test ${title} because server version ${this.#VERSION_NUMBERS.join('.')} is not within range ${range[0].join(".")} - ${range[1] !== 'LATEST' ? range[1].join(".") : 'LATEST'}`) } +} testWithClientPool< M extends RedisModules = {}, From 9459660d960bb015b67d4bdf80f20672d85cb1ed Mon Sep 17 00:00:00 2001 From: Hristo Temelski Date: Wed, 30 Apr 2025 15:57:01 +0300 Subject: [PATCH 071/105] fix(pubsub): Fixed cluster client pubsub logic * Infer the cluster pubsub client read only mode from the node type * Modify flag logic --- packages/client/lib/cluster/cluster-slots.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/lib/cluster/cluster-slots.ts b/packages/client/lib/cluster/cluster-slots.ts index 3a4adff73c4..c2fde197f4f 100644 --- a/packages/client/lib/cluster/cluster-slots.ts +++ b/packages/client/lib/cluster/cluster-slots.ts @@ -518,7 +518,7 @@ export default class RedisClusterSlots< node = index < this.masters.length ? this.masters[index] : this.replicas[index - this.masters.length], - client = this.#createClient(node, true); + client = this.#createClient(node, index >= this.masters.length); this.pubSubNode = { address: node.address, From 49d6b2d465cf1e2aa5792bcd2bec5ff058c4545b Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Wed, 30 Apr 2025 16:28:22 +0300 Subject: [PATCH 072/105] Update README.MD (#2924) * Update README.MD * docs: update programmability.md examples + add Programmability section to README and * fix imports according to the new v5 exports * more v5 docs updates --------- Co-authored-by: Nikolay Karadzhov --- README.md | 259 +++++++++++++++++++++++++++++++++++++++- docs/RESP.md | 2 +- docs/programmability.md | 33 +++-- docs/v4-to-v5.md | 12 +- docs/v5.md | 65 +++++++++- 5 files changed, 341 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 8e7e3845256..b9fe524c677 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,10 @@ node-redis is a modern, high performance [Redis](https://redis.io) client for No ## Installation -Start a redis-server via docker (or any other method you prefer): +Start a redis via docker: ```bash -docker run -p 6379:6379 -it redis/redis-stack-server:latest +docker run -p 6379:6379 -d redis:8.0-rc1 ``` To install node-redis, simply: @@ -38,13 +38,13 @@ To install node-redis, simply: ```bash npm install redis ``` - -> "redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands, you can install the individual packages. See the list below. +> "redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands, +> you can install the individual packages. See the list below. ## Packages | Name | Description | -|------------------------------------------------|---------------------------------------------------------------------------------------------| +| ---------------------------------------------- | ------------------------------------------------------------------------------------------- | | [`redis`](./packages/redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | | [`@redis/client`](./packages/client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | | [`@redis/bloom`](./packages/bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | @@ -53,7 +53,254 @@ npm install redis | [`@redis/time-series`](./packages/time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | | [`@redis/entraid`](./packages/entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | -> Looking for a high-level library to handle object mapping? See [redis-om-node](https://github.com/redis/redis-om-node)! +> Looking for a high-level library to handle object mapping? +> See [redis-om-node](https://github.com/redis/redis-om-node)! + + +## Usage + +### Basic Example + +```typescript +import { createClient } from "redis"; + +const client = await createClient() + .on("error", (err) => console.log("Redis Client Error", err)) + .connect(); + +await client.set("key", "value"); +const value = await client.get("key"); +client.destroy(); +``` + +The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in +the format `redis[s]://[[username][:password]@][host][:port][/db-number]`: + +```typescript +createClient({ + url: "redis://alice:foobared@awesome.redis.server:6380", +}); +``` + +You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in +the [client configuration guide](./docs/client-configuration.md). + +To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean. +`client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it +isn't (for example when the client is still connecting or reconnecting after a network error). + +### Redis Commands + +There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed +using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`, +etc.): + +```typescript +// raw Redis commands +await client.HSET("key", "field", "value"); +await client.HGETALL("key"); + +// friendly JavaScript commands +await client.hSet("key", "field", "value"); +await client.hGetAll("key"); +``` + +Modifiers to commands are specified using a JavaScript object: + +```typescript +await client.set("key", "value", { + EX: 10, + NX: true, +}); +``` + +Replies will be transformed into useful data structures: + +```typescript +await client.hGetAll("key"); // { field1: 'value1', field2: 'value2' } +await client.hVals("key"); // ['value1', 'value2'] +``` + +`Buffer`s are supported as well: + +```typescript +const client = createClient().withTypeMapping({ + [RESP_TYPES.BLOB_STRING]: Buffer +}); + +await client.hSet("key", "field", Buffer.from("value")); // 'OK' +await client.hGet("key", "field"); // { field: } + +``` + +### Unsupported Redis Commands + +If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`: + +```typescript +await client.sendCommand(["SET", "key", "value", "NX"]); // 'OK' + +await client.sendCommand(["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2'] +``` + +### Transactions (Multi/Exec) + +Start a [transaction](https://redis.io/topics/transactions) by calling `.multi()`, then chaining your commands. When +you're done, call `.exec()` and you'll get an array back with your results: + +```typescript +await client.set("another-key", "another-value"); + +const [setKeyReply, otherKeyValue] = await client + .multi() + .set("key", "value") + .get("another-key") + .exec(); // ['OK', 'another-value'] +``` + +You can also [watch](https://redis.io/topics/transactions#optimistic-locking-using-check-and-set) keys by calling +`.watch()`. Your transaction will abort if any of the watched keys change. + + +### Blocking Commands + +In v4, `RedisClient` had the ability to create a pool of connections using an "Isolation Pool" on top of the "main" +connection. However, there was no way to use the pool without a "main" connection: + +```javascript +const client = await createClient() + .on("error", (err) => console.error(err)) + .connect(); + +await client.ping(client.commandOptions({ isolated: true })); +``` + +In v5 we've extracted this pool logic into its own class—`RedisClientPool`: + +```javascript +const pool = await createClientPool() + .on("error", (err) => console.error(err)) + .connect(); + +await pool.ping(); +``` + + +### Pub/Sub + +See the [Pub/Sub overview](./docs/pub-sub.md). + +### Scan Iterator + +[`SCAN`](https://redis.io/commands/scan) results can be looped over +using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator): + +```typescript +for await (const key of client.scanIterator()) { + // use the key! + await client.get(key); +} +``` + +This works with `HSCAN`, `SSCAN`, and `ZSCAN` too: + +```typescript +for await (const { field, value } of client.hScanIterator("hash")) { +} +for await (const member of client.sScanIterator("set")) { +} +for await (const { score, value } of client.zScanIterator("sorted-set")) { +} +``` + +You can override the default options by providing a configuration object: + +```typescript +client.scanIterator({ + TYPE: "string", // `SCAN` only + MATCH: "patter*", + COUNT: 100, +}); +``` + +### Disconnecting + +The `QUIT` command has been deprecated in Redis 7.2 and should now also be considered deprecated in Node-Redis. Instead +of sending a `QUIT` command to the server, the client can simply close the network connection. + +`client.QUIT/quit()` is replaced by `client.close()`. and, to avoid confusion, `client.disconnect()` has been renamed to +`client.destroy()`. + +```typescript +client.destroy(); +``` + +### Auto-Pipelining + +Node Redis will automatically pipeline requests that are made during the same "tick". + +```typescript +client.set("Tm9kZSBSZWRpcw==", "users:1"); +client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw=="); +``` + +Of course, if you don't do something with your Promises you're certain to +get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take +advantage of auto-pipelining and handle your Promises, use `Promise.all()`. + +```typescript +await Promise.all([ + client.set("Tm9kZSBSZWRpcw==", "users:1"), + client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw=="), +]); +``` + +### Programmability + +See the [Programmability overview](./docs/programmability.md). + +### Clustering + +Check out the [Clustering Guide](./docs/clustering.md) when using Node Redis to connect to a Redis Cluster. + +### Events + +The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes: + +| Name | When | Listener arguments | +| ----------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------- | +| `connect` | Initiating a connection to the server | _No arguments_ | +| `ready` | Client is ready to use | _No arguments_ | +| `end` | Connection has been closed (via `.disconnect()`) | _No arguments_ | +| `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` | +| `reconnecting` | Client is trying to reconnect to the server | _No arguments_ | +| `sharded-channel-moved` | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | See [here](./docs/pub-sub.md#sharded-channel-moved-event) | + +> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and +> an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details. + +> The client will not emit [any other events](./docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. + +## Supported Redis versions + +Node Redis is supported with the following versions of Redis: + +| Version | Supported | +| ------- | ------------------ | +| 8.0.z | :heavy_check_mark: | +| 7.0.z | :heavy_check_mark: | +| 6.2.z | :heavy_check_mark: | +| 6.0.z | :heavy_check_mark: | +| 5.0.z | :heavy_check_mark: | +| < 5.0 | :x: | + +> Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support. + +## Migration + +- [From V3 to V4](docs/v3-to-v4.md) +- [From V4 to V5](docs/v4-to-v5.md) +- [V5](docs/v5.md) ## Contributing diff --git a/docs/RESP.md b/docs/RESP.md index 5d634831e17..f8c2388226b 100644 --- a/docs/RESP.md +++ b/docs/RESP.md @@ -23,7 +23,7 @@ By default, each type is mapped to the first option in the lists below. To chang - Double (`,`) => `number | string` - Simple String (`+`) => `string | Buffer` - Blob String (`$`) => `string | Buffer` -- Verbatim String (`=`) => `string | Buffer | VerbatimString` (TODO: `VerbatimString` typedoc link) +- Verbatim String (`=`) => `string | Buffer | VerbatimString` - Simple Error (`-`) => `ErrorReply` - Blob Error (`!`) => `ErrorReply` - Array (`*`) => `Array` diff --git a/docs/programmability.md b/docs/programmability.md index f6ae42033c6..56eb048ca0c 100644 --- a/docs/programmability.md +++ b/docs/programmability.md @@ -25,16 +25,22 @@ FUNCTION LOAD "#!lua name=library\nredis.register_function{function_name='add', Load the prior redis function on the _redis server_ before running the example below. ```typescript -import { createClient } from 'redis'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { NumberReply } from '@redis/client/lib/RESP/types'; +import { createClient, RedisArgument } from 'redis'; const client = createClient({ functions: { library: { add: { NUMBER_OF_KEYS: 1, - FIRST_KEY_INDEX: 1, - transformArguments(key: string, toAdd: number): Array { - return [key, toAdd.toString()]; + parseCommand( + parser: CommandParser, + key: RedisArgument, + toAdd: RedisArgument + ) { + parser.pushKey(key) + parser.push(toAdd) }, transformReply: undefined as unknown as () => NumberReply } @@ -43,9 +49,8 @@ const client = createClient({ }); await client.connect(); - await client.set('key', '1'); -await client.library.add('key', 2); // 3 +await client.library.add('key', '2'); // 3 ``` ## [Lua Scripts](https://redis.io/docs/manual/programmability/eval-intro/) @@ -53,7 +58,9 @@ await client.library.add('key', 2); // 3 The following is an end-to-end example of the prior concept. ```typescript -import { createClient, defineScript, NumberReply } from 'redis'; +import { CommandParser } from '@redis/client/lib/client/parser'; +import { NumberReply } from '@redis/client/lib/RESP/types'; +import { createClient, defineScript, RedisArgument } from 'redis'; const client = createClient({ scripts: { @@ -61,8 +68,13 @@ const client = createClient({ SCRIPT: 'return redis.call("GET", KEYS[1]) + ARGV[1];', NUMBER_OF_KEYS: 1, FIRST_KEY_INDEX: 1, - transformArguments(key: string, toAdd: number): Array { - return [key, toAdd.toString()]; + parseCommand( + parser: CommandParser, + key: RedisArgument, + toAdd: RedisArgument + ) { + parser.pushKey(key) + parser.push(toAdd) }, transformReply: undefined as unknown as () => NumberReply }) @@ -70,7 +82,6 @@ const client = createClient({ }); await client.connect(); - await client.set('key', '1'); -await client.add('key', 2); // 3 +await client.add('key', '2'); // 3 ``` diff --git a/docs/v4-to-v5.md b/docs/v4-to-v5.md index 3b09658b66f..fbe02e7c4d6 100644 --- a/docs/v4-to-v5.md +++ b/docs/v4-to-v5.md @@ -234,12 +234,12 @@ In v5, any unwritten commands (in the same pipeline) will be discarded. ### Time Series - `TS.ADD`: `boolean` -> `number` [^boolean-to-number] -- `TS.[M][REV]RANGE`: `enum TimeSeriesBucketTimestamp` -> `const TIME_SERIES_BUCKET_TIMESTAMP` [^enum-to-constants], `enum TimeSeriesReducers` -> `const TIME_SERIES_REDUCERS` [^enum-to-constants], the `ALIGN` argument has been moved into `AGGREGRATION` +- `TS.[M][REV]RANGE`: the `ALIGN` argument has been moved into `AGGREGATION` - `TS.SYNUPDATE`: `Array>` -> `Record>` -- `TS.M[REV]RANGE[_WITHLABELS]`, `TS.MGET[_WITHLABELS]`: TODO - -[^enum-to-constants]: TODO +- `TimeSeriesDuplicatePolicies` -> `TIME_SERIES_DUPLICATE_POLICIES` [^enum-to-constants] +- `TimeSeriesEncoding` -> `TIME_SERIES_ENCODING` [^enum-to-constants] +- `TimeSeriesAggregationType` -> `TIME_SERIES_AGGREGATION_TYPE` [^enum-to-constants] +- `TimeSeriesReducers` -> `TIME_SERIES_REDUCERS` [^enum-to-constants] +- `TimeSeriesBucketTimestamp` -> `TIME_SERIES_BUCKET_TIMESTAMP` [^enum-to-constants] [^map-keys]: To avoid unnecessary transformations and confusion, map keys will not be transformed to "js friendly" names (i.e. `number-of-keys` will not be renamed to `numberOfKeys`). See [here](https://github.com/redis/node-redis/discussions/2506). - -[^future-proofing]: TODO diff --git a/docs/v5.md b/docs/v5.md index 4a1bd817b9b..a3d0ab68389 100644 --- a/docs/v5.md +++ b/docs/v5.md @@ -1,30 +1,83 @@ # RESP3 Support -TODO +Node Redis v5 adds support for [RESP3](https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md), the new Redis serialization protocol. RESP3 offers richer data types and improved type handling compared to RESP2. + +To use RESP3, specify it when creating your client: ```javascript +import { createClient } from 'redis'; + const client = createClient({ RESP: 3 }); ``` +## Type Mapping + +With RESP3, you can leverage the protocol's richer type system. You can customize how different Redis types are represented in JavaScript using type mapping: + ```javascript -// by default +import { createClient, RESP_TYPES } from 'redis'; + +// By default await client.hGetAll('key'); // Record +// Use Map instead of plain object await client.withTypeMapping({ - [TYPES.MAP]: Map + [RESP_TYPES.MAP]: Map }).hGetAll('key'); // Map +// Use both Map and Buffer await client.withTypeMapping({ - [TYPES.MAP]: Map, - [TYPES.BLOB_STRING]: Buffer + [RESP_TYPES.MAP]: Map, + [RESP_TYPES.BLOB_STRING]: Buffer }).hGetAll('key'); // Map ``` +This replaces the previous approach of using `commandOptions({ returnBuffers: true })` in v4. + +## PubSub in RESP3 + +RESP3 uses a different mechanism for handling Pub/Sub messages. Instead of modifying the `onReply` handler as in RESP2, RESP3 provides a dedicated `onPush` handler. When using RESP3, the client automatically uses this more efficient push notification system. + +## Known Limitations + +### Unstable Module Commands + +Some Redis module commands have unstable RESP3 transformations. These commands will throw an error when used with RESP3 unless you explicitly opt in to using them by setting `unstableResp3: true` in your client configuration: + +```javascript +const client = createClient({ + RESP: 3, + unstableResp3: true +}); +``` + +The following commands have unstable RESP3 implementations: + +1. **Stream Commands**: + - `XREAD` and `XREADGROUP` - The response format differs between RESP2 and RESP3 + +2. **Search Commands (RediSearch)**: + - `FT.AGGREGATE` + - `FT.AGGREGATE_WITHCURSOR` + - `FT.CURSOR_READ` + - `FT.INFO` + - `FT.PROFILE_AGGREGATE` + - `FT.PROFILE_SEARCH` + - `FT.SEARCH` + - `FT.SEARCH_NOCONTENT` + - `FT.SPELLCHECK` + +3. **Time Series Commands**: + - `TS.INFO` + - `TS.INFO_DEBUG` + +If you need to use these commands with RESP3, be aware that the response format might change in future versions. + # Sentinel Support -[TODO](./sentinel.md) +[Sentinel](./sentinel.md) # `multi.exec<'typed'>` / `multi.execTyped` From 46bfeaa94e8987216fcfd554c8ef7c5159480429 Mon Sep 17 00:00:00 2001 From: "Bobby I." Date: Wed, 30 Apr 2025 16:30:16 +0300 Subject: [PATCH 073/105] Fix typo and improve Sentinel docs (#2931) --- docs/sentinel.md | 31 +++++---- packages/client/lib/sentinel/index.spec.ts | 38 +++++------ packages/client/lib/sentinel/index.ts | 75 +++++++++++++++++++++- packages/client/lib/sentinel/types.ts | 10 ++- 4 files changed, 118 insertions(+), 36 deletions(-) diff --git a/docs/sentinel.md b/docs/sentinel.md index 80e79c3f88c..f10b2953df5 100644 --- a/docs/sentinel.md +++ b/docs/sentinel.md @@ -14,7 +14,7 @@ const sentinel = await createSentinel({ port: 1234 }] }) - .on('error', err => console.error('Redis Sentinel Error', err)); + .on('error', err => console.error('Redis Sentinel Error', err)) .connect(); await sentinel.set('key', 'value'); @@ -26,16 +26,19 @@ In the above example, we configure the sentinel object to fetch the configuratio ## `createSentinel` configuration -| Property | Default | Description | -|-----------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| name | | The sentinel identifier for a particular database cluster | -| sentinelRootNodes | | An array of root nodes that are part of the sentinel cluster, which will be used to get the topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the sentinel configuration from the server | -| maxCommandRediscovers | `16` | The maximum number of times a command will retry due to topology changes. | -| nodeClientOptions | | The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with | -| sentinelClientOptions | | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with | -| masterPoolSize | `1` | The number of clients connected to the master node | -| replicaPoolSize | `0` | The number of clients connected to each replica node. When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes. | -| reserveClient | `false` | When `true`, one client will be reserved for the sentinel object. When `false`, the sentinel object will wait for the first available client from the pool. | +| Property | Default | Description | +|----------------------------|-----------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| name | | The sentinel identifier for a particular database cluster | +| sentinelRootNodes | | An array of root nodes that are part of the sentinel cluster, which will be used to get the topology. Each element in the array is a client configuration object. There is no need to specify every node in the cluster: 3 should be enough to reliably connect and obtain the sentinel configuration from the server | +| maxCommandRediscovers | `16` | The maximum number of times a command will retry due to topology changes. | +| nodeClientOptions | | The configuration values for every node in the cluster. Use this for example when specifying an ACL user to connect with | +| sentinelClientOptions | | The configuration values for every sentinel in the cluster. Use this for example when specifying an ACL user to connect with | +| masterPoolSize | `1` | The number of clients connected to the master node | +| replicaPoolSize | `0` | The number of clients connected to each replica node. When greater than 0, the client will distribute the load by executing read-only commands (such as `GET`, `GEOSEARCH`, etc.) across all the cluster nodes. | +| scanInterval | `10000` | Interval in milliseconds to periodically scan for changes in the sentinel topology. The client will query the sentinel for changes at this interval. | +| passthroughClientErrorEvents | `false` | When `true`, error events from client instances inside the sentinel will be propagated to the sentinel instance. This allows handling all client errors through a single error handler on the sentinel instance. | +| reserveClient | `false` | When `true`, one client will be reserved for the sentinel object. When `false`, the sentinel object will wait for the first available client from the pool. | + ## PubSub It supports PubSub via the normal mechanisms, including migrating the listeners if the node they are connected to goes down. @@ -60,7 +63,7 @@ createSentinel({ }); ``` -In addition, it also provides the ability have a pool of clients connected to the replica nodes, and to direct all read-only commands to them: +In addition, it also provides the ability have a pool of clients connected to the replica nodes, and to direct all read-only commands to them: ```javascript createSentinel({ @@ -85,9 +88,9 @@ const result = await sentinel.use(async client => { }); ``` -`.getMasterClientLease()` +`.acquire()` ```javascript -const clientLease = await sentinel.getMasterClientLease(); +const clientLease = await sentinel.acquire(); try { await clientLease.watch('key'); diff --git a/packages/client/lib/sentinel/index.spec.ts b/packages/client/lib/sentinel/index.spec.ts index 567da4b1a50..cf9228c261f 100644 --- a/packages/client/lib/sentinel/index.spec.ts +++ b/packages/client/lib/sentinel/index.spec.ts @@ -134,7 +134,7 @@ describe(`test with scripts`, () => { }, GLOBAL.SENTINEL.WITH_SCRIPT); testUtils.testWithClientSentinel('with script multi', async sentinel => { - const reply = await sentinel.multi().set('key', 2).square('key').exec(); + const reply = await sentinel.multi().set('key', 2).square('key').exec(); assert.deepEqual(reply, ['OK', 4]); }, GLOBAL.SENTINEL.WITH_SCRIPT); @@ -148,7 +148,7 @@ describe(`test with scripts`, () => { ); }, GLOBAL.SENTINEL.WITH_SCRIPT) }); - + describe(`test with functions`, () => { testUtils.testWithClientSentinel('with function', async sentinel => { @@ -178,14 +178,14 @@ describe(`test with functions`, () => { MATH_FUNCTION.code, { REPLACE: true } ); - + const reply = await sentinel.use( async (client: any) => { await client.set('key', '2'); return client.math.square('key'); } ); - + assert.equal(reply, 4); }, GLOBAL.SENTINEL.WITH_FUNCTION); }); @@ -216,7 +216,7 @@ describe(`test with replica pool size 1`, () => { testUtils.testWithClientSentinel('client lease', async sentinel => { sentinel.on("error", () => { }); - const clientLease = await sentinel.aquire(); + const clientLease = await sentinel.acquire(); clientLease.set('x', 456); let matched = false; @@ -243,7 +243,7 @@ describe(`test with replica pool size 1`, () => { return await client.get("x"); } ) - + await sentinel.set("x", 1); assert.equal(await promise, null); }, GLOBAL.SENTINEL.WITH_REPLICA_POOL_SIZE_1); @@ -276,7 +276,7 @@ describe(`test with masterPoolSize 2, reserve client true`, () => { assert.equal(await promise2, "2"); }, Object.assign(GLOBAL.SENTINEL.WITH_RESERVE_CLIENT_MASTER_POOL_SIZE_2, {skipTest: true})); }); - + describe(`test with masterPoolSize 2`, () => { testUtils.testWithClientSentinel('multple clients', async sentinel => { sentinel.on("error", () => { }); @@ -313,14 +313,14 @@ describe(`test with masterPoolSize 2`, () => { }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); testUtils.testWithClientSentinel('lease - watch - clean', async sentinel => { - const leasedClient = await sentinel.aquire(); + const leasedClient = await sentinel.acquire(); await leasedClient.set('x', 1); await leasedClient.watch('x'); assert.deepEqual(await leasedClient.multi().get('x').exec(), ['1']) }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); testUtils.testWithClientSentinel('lease - watch - dirty', async sentinel => { - const leasedClient = await sentinel.aquire(); + const leasedClient = await sentinel.acquire(); await leasedClient.set('x', 1); await leasedClient.watch('x'); await leasedClient.set('x', 2); @@ -328,11 +328,11 @@ describe(`test with masterPoolSize 2`, () => { await assert.rejects(leasedClient.multi().get('x').exec(), new WatchError()); }, GLOBAL.SENTINEL.WITH_MASTER_POOL_SIZE_2); }); - + // TODO: Figure out how to modify the test utils // so it would have fine grained controll over -// sentinel +// sentinel // it should somehow replicate the `SentinelFramework` object functionallities async function steadyState(frame: SentinelFramework) { let checkedMaster = false; @@ -439,7 +439,7 @@ describe.skip('legacy tests', () => { sentinel.on('error', () => { }); } - if (this!.currentTest!.state === 'failed') { + if (this!.currentTest!.state === 'failed') { console.log(`longest event loop blocked delta: ${longestDelta}`); console.log(`longest event loop blocked in failing test: ${longestTestDelta}`); console.log("trace:"); @@ -454,7 +454,7 @@ describe.skip('legacy tests', () => { frame.sentinelMaster(), frame.sentinelReplicas() ]) - + console.log(`sentinel sentinels:\n${JSON.stringify(results[0], undefined, '\t')}`); console.log(`sentinel master:\n${JSON.stringify(results[1], undefined, '\t')}`); console.log(`sentinel replicas:\n${JSON.stringify(results[2], undefined, '\t')}`); @@ -492,7 +492,7 @@ describe.skip('legacy tests', () => { ); }); - // stops master to force sentinel to update + // stops master to force sentinel to update it('stop master', async function () { this.timeout(60000); @@ -538,8 +538,8 @@ describe.skip('legacy tests', () => { tracer.push("connected"); - const client = await sentinel.aquire(); - tracer.push("aquired lease"); + const client = await sentinel.acquire(); + tracer.push("acquired lease"); await client.set("x", 1); await client.watch("x"); @@ -586,7 +586,7 @@ describe.skip('legacy tests', () => { await sentinel.connect(); tracer.push("connected"); - const client = await sentinel.aquire(); + const client = await sentinel.acquire(); tracer.push("got leased client"); await client.set("x", 1); await client.watch("x"); @@ -965,10 +965,10 @@ describe.skip('legacy tests', () => { tracer.push("adding node"); await frame.addNode(); tracer.push("added node and waiting on added promise"); - await nodeAddedPromise; + await nodeAddedPromise; }) }); }); - + diff --git a/packages/client/lib/sentinel/index.ts b/packages/client/lib/sentinel/index.ts index 73c4fffd070..3bf94abd819 100644 --- a/packages/client/lib/sentinel/index.ts +++ b/packages/client/lib/sentinel/index.ts @@ -32,14 +32,30 @@ export class RedisSentinelClient< #internal: RedisSentinelInternal; readonly _self: RedisSentinelClient; + /** + * Indicates if the client connection is open + * + * @returns `true` if the client connection is open, `false` otherwise + */ + get isOpen() { return this._self.#internal.isOpen; } + /** + * Indicates if the client connection is ready to accept commands + * + * @returns `true` if the client connection is ready, `false` otherwise + */ get isReady() { return this._self.#internal.isReady; } + /** + * Gets the command options configured for this client + * + * @returns The command options for this client or `undefined` if none were set + */ get commandOptions() { return this._self.#commandOptions; } @@ -222,6 +238,16 @@ export class RedisSentinelClient< unwatch = this.UNWATCH; + /** + * Releases the client lease back to the pool + * + * After calling this method, the client instance should no longer be used as it + * will be returned to the client pool and may be given to other operations. + * + * @returns A promise that resolves when the client is ready to be reused, or undefined + * if the client was immediately ready + * @throws Error if the lease has already been released + */ release() { if (this._self.#clientInfo === undefined) { throw new Error('RedisSentinelClient lease already released'); @@ -245,10 +271,20 @@ export default class RedisSentinel< #internal: RedisSentinelInternal; #options: RedisSentinelOptions; + /** + * Indicates if the sentinel connection is open + * + * @returns `true` if the sentinel connection is open, `false` otherwise + */ get isOpen() { return this._self.#internal.isOpen; } + /** + * Indicates if the sentinel connection is ready to accept commands + * + * @returns `true` if the sentinel connection is ready, `false` otherwise + */ get isReady() { return this._self.#internal.isReady; } @@ -511,7 +547,28 @@ export default class RedisSentinel< pUnsubscribe = this.PUNSUBSCRIBE; - async aquire(): Promise> { + /** + * Acquires a master client lease for exclusive operations + * + * Used when multiple commands need to run on an exclusive client (for example, using `WATCH/MULTI/EXEC`). + * The returned client must be released after use with the `release()` method. + * + * @returns A promise that resolves to a Redis client connected to the master node + * @example + * ```javascript + * const clientLease = await sentinel.acquire(); + * + * try { + * await clientLease.watch('key'); + * const resp = await clientLease.multi() + * .get('key') + * .exec(); + * } finally { + * clientLease.release(); + * } + * ``` + */ + async acquire(): Promise> { const clientInfo = await this._self.#internal.getClientLease(); return RedisSentinelClient.create(this._self.#options, this._self.#internal, clientInfo, this._self.#commandOptions); } @@ -641,6 +698,12 @@ class RedisSentinelInternal< }); } + /** + * Gets a client lease from the master client pool + * + * @returns A client info object or a promise that resolves to a client info object + * when a client becomes available + */ getClientLease(): ClientInfo | Promise { const id = this.#masterClientQueue.shift(); if (id !== undefined) { @@ -650,6 +713,16 @@ class RedisSentinelInternal< return this.#masterClientQueue.wait().then(id => ({ id })); } + /** + * Releases a client lease back to the pool + * + * If the client was used for a transaction that might have left it in a dirty state, + * it will be reset before being returned to the pool. + * + * @param clientInfo The client info object representing the client to release + * @returns A promise that resolves when the client is ready to be reused, or undefined + * if the client was immediately ready or no longer exists + */ releaseClientLease(clientInfo: ClientInfo) { const client = this.#masterClients[clientInfo.id]; // client can be undefined if releasing in middle of a reconfigure diff --git a/packages/client/lib/sentinel/types.ts b/packages/client/lib/sentinel/types.ts index 428e7bccd66..28a5a91ddd3 100644 --- a/packages/client/lib/sentinel/types.ts +++ b/packages/client/lib/sentinel/types.ts @@ -49,11 +49,17 @@ export interface RedisSentinelOptions< */ replicaPoolSize?: number; /** - * TODO + * Interval in milliseconds to periodically scan for changes in the sentinel topology. + * The client will query the sentinel for changes at this interval. + * + * Default: 10000 (10 seconds) */ scanInterval?: number; /** - * TODO + * When `true`, error events from client instances inside the sentinel will be propagated to the sentinel instance. + * This allows handling all client errors through a single error handler on the sentinel instance. + * + * Default: false */ passthroughClientErrorEvents?: boolean; /** From 4022e6947d23c3938c62578eae3b3d5b71cc5c0e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 16:50:35 +0300 Subject: [PATCH 074/105] update package-lock.json (#2932) --- package-lock.json | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 064b1158f50..dee35a0fcec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,6 @@ "workspaces": [ "./packages/*" ], - "dependencies": { - "ts-node": "^10.9.2" - }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.2", "@types/mocha": "^10.0.6", @@ -19,6 +16,7 @@ "mocha": "^10.2.0", "nyc": "^15.1.0", "release-it": "^17.0.3", + "ts-node": "^10.9.2", "tsx": "^4.7.0", "typedoc": "^0.25.7", "typescript": "^5.3.3" @@ -671,6 +669,7 @@ "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.9" @@ -683,6 +682,7 @@ "version": "0.3.9", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", @@ -829,6 +829,7 @@ }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.1", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -844,6 +845,7 @@ }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", + "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { @@ -1187,24 +1189,28 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node12": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node14": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, "license": "MIT" }, "node_modules/@tsconfig/node16": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, "license": "MIT" }, "node_modules/@types/body-parser": { @@ -1290,6 +1296,7 @@ }, "node_modules/@types/node": { "version": "20.11.16", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -1376,6 +1383,7 @@ "version": "8.14.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "dev": true, "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1388,6 +1396,7 @@ "version": "8.3.4", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, "license": "MIT", "dependencies": { "acorn": "^8.11.0" @@ -1518,6 +1527,7 @@ "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -2391,6 +2401,7 @@ "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, "license": "MIT" }, "node_modules/cross-spawn": { @@ -5170,6 +5181,7 @@ "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, "license": "ISC" }, "node_modules/marked": { @@ -7664,6 +7676,7 @@ "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, "license": "MIT", "dependencies": { "@cspotcode/source-map-support": "^0.8.0", @@ -7707,6 +7720,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -7877,6 +7891,7 @@ }, "node_modules/typescript": { "version": "5.3.3", + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -7915,6 +7930,7 @@ }, "node_modules/undici-types": { "version": "5.26.5", + "dev": true, "license": "MIT" }, "node_modules/unicorn-magic": { @@ -8094,6 +8110,7 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, "license": "MIT" }, "node_modules/vary": { @@ -8468,6 +8485,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -8535,7 +8553,7 @@ "version": "5.0.0-next.7", "license": "MIT", "dependencies": { - "@azure/identity": "4.7.0", + "@azure/identity": "^4.7.0", "@azure/msal-node": "^2.16.1" }, "devDependencies": { From bf2b3752d6697f8d2152d2162be8d86c8890d7eb Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 16:56:53 +0300 Subject: [PATCH 075/105] Release client@5.0.0 --- packages/client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/package.json b/packages/client/package.json index 3372f3a5758..5b88c185da9 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 091244a32a8f983b6678699f409687563c4e3796 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:00:26 +0300 Subject: [PATCH 076/105] Updated the Bloom package to use client@5.0.0 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index d913d434a15..a1916660749 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" }, "devDependencies": { "@redis/test-utils": "*" From bd10c92348e8b5592ea64ca7e11ea741834f7b04 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:04:03 +0300 Subject: [PATCH 077/105] Release bloom@5.0.0 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index a1916660749..595e581ad8e 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 42911295a4710634b357cc8244f837014b770707 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:05:10 +0300 Subject: [PATCH 078/105] Updated the Entraid package to use client@5.0.0 --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 4eba7e0a788..7c61a729ba9 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -21,7 +21,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" }, "devDependencies": { "@types/express": "^4.17.21", From 99003307f986bcc6b6e77149af61e62b4d02132d Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:05:46 +0300 Subject: [PATCH 079/105] Release entraid@5.0.0 --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 7c61a729ba9..8e9ff16aa22 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 73596170471c6b588c5ee5fd0977cc6049b6b24d Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:06:44 +0300 Subject: [PATCH 080/105] Updated the Json package to use client@5.0.0 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index 0217aba33c6..378fabb080c 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" }, "devDependencies": { "@redis/test-utils": "*" From b7147911de0ffa725df48dc859156b658a2d85f0 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:07:55 +0300 Subject: [PATCH 081/105] Release json@5.0.0 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index 378fabb080c..7d6163cc59e 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From c942f0eb9f77e818d537dc49974cc7d316117b8e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:12:59 +0300 Subject: [PATCH 082/105] Updated the Search package to use client@5.0.0 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index 605ab0a7ffe..c670f6e6aea 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -13,7 +13,7 @@ "test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" }, "devDependencies": { "@redis/test-utils": "*" From 30cecc4b48100d92fda8b18fd13828b179e5d8c7 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:13:28 +0300 Subject: [PATCH 083/105] Release search@5.0.0 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index c670f6e6aea..0097cce3480 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 912e0d8170a7ea7760d1bdf36d3e3f0012d97304 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:14:39 +0300 Subject: [PATCH 084/105] Updated the Timeseries package to use client@5.0.0 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index e0c138b0dd2..bc14593d756 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" }, "devDependencies": { "@redis/test-utils": "*" From 16fb7e02da469f06ba52cadc79ac5f07b27a7848 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:15:15 +0300 Subject: [PATCH 085/105] Release time-series@5.0.0 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index bc14593d756..5960350ff41 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 1daf0f02dacdc1f875bcb068e11141f0856c1d0e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Wed, 30 Apr 2025 17:17:15 +0300 Subject: [PATCH 086/105] Updated the Redis package to use client@5.0.0 --- packages/redis/package.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/redis/package.json b/packages/redis/package.json index 8b1567afb1d..7e1c38fadbf 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -10,11 +10,11 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { - "@redis/bloom": "5.0.0-next.7", - "@redis/client": "5.0.0-next.7", - "@redis/json": "5.0.0-next.7", - "@redis/search": "5.0.0-next.7", - "@redis/time-series": "5.0.0-next.7" + "@redis/bloom": "5.0.0", + "@redis/client": "5.0.0", + "@redis/json": "5.0.0", + "@redis/search": "5.0.0", + "@redis/time-series": "5.0.0" }, "engines": { "node": ">= 18" From 47e297077a20f396a1b22fafa917cea284dc3427 Mon Sep 17 00:00:00 2001 From: "H. Temelski" Date: Wed, 30 Apr 2025 17:20:34 +0300 Subject: [PATCH 087/105] Release redis@5.0.0 --- packages/redis/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/redis/package.json b/packages/redis/package.json index 7e1c38fadbf..ddb79ca2275 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 964b8de06d983b44dfda861dc966b3fca00a01ea Mon Sep 17 00:00:00 2001 From: Aryan Pandey Date: Sat, 3 May 2025 19:02:56 +0530 Subject: [PATCH 088/105] docs: clarify connection pooling in createClient and fix broken link in isolationPoolOptions (#2896) - Added a Connection Pooling section in `createClient` documentation to clarify that a single connection is typically sufficient and to provide guidance on when to use a connection pool. - Updated `isolationPoolOptions` description with a more precise explanation and replaced the broken link with a reference to `createClientPool`. - Changes made based on issue #2845. --- docs/client-configuration.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/client-configuration.md b/docs/client-configuration.md index deb68437e16..0564794ac46 100644 --- a/docs/client-configuration.md +++ b/docs/client-configuration.md @@ -25,7 +25,7 @@ | disableOfflineQueue | `false` | Disables offline queuing, see [FAQ](./FAQ.md#what-happens-when-the-network-goes-down) | | 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) | +| isolationPoolOptions | | An object that configures a pool of isolated connections, If you frequently need isolated connections, consider using [createClientPool](https://github.com/redis/node-redis/blob/master/docs/pool.md#creating-a-pool) instead | | pingInterval | | Send `PING` command at interval (in ms). Useful with ["Azure Cache for Redis"](https://learn.microsoft.com/en-us/azure/azure-cache-for-redis/cache-best-practices-connection#idle-timeout) | ## Reconnect Strategy @@ -81,3 +81,9 @@ createClient({ } }); ``` +## Connection Pooling + +In most cases, a single Redis connection is sufficient, as the node-redis client efficiently handles commands using an underlying socket. Unlike traditional databases, Redis does not require connection pooling for optimal performance. + +However, if your use case requires exclusive connections see [RedisClientPool](https://github.com/redis/node-redis/blob/master/docs/pool.md), which allows you to create and manage multiple dedicated connections. + From bab1211ad499e2a6bdd1b62b07481ccfcc156344 Mon Sep 17 00:00:00 2001 From: Nicholas Wilson Date: Sat, 3 May 2025 08:34:15 -0500 Subject: [PATCH 089/105] Updated CHANGELOG.md, fix typo(s) (#2861) --- CHANGELOG.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33cf69851c7..fbc3070381e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,7 +21,7 @@ - Fix `NOAUTH` error when using authentication & database (#1681) - Allow to `.quit()` in PubSub mode (#1766) -- Add an option to configurate `name` on a client (#1758) +- Add an option to configure `name` on a client (#1758) - Lowercase commands (`client.hset`) in `legacyMode` - Fix PubSub resubscribe (#1764) - Fix `RedisSocketOptions` type (#1741) @@ -466,7 +466,7 @@ Features Bugfixes - Fixed a javascript parser regression introduced in 2.0 that could result in timeouts on high load. ([@BridgeAR](https://github.com/BridgeAR)) -- I was not able to write a regression test for this, since the error seems to only occur under heavy load with special conditions. So please have a look for timeouts with the js parser, if you use it and report all issues and switch to the hiredis parser in the meanwhile. If you're able to come up with a reproducable test case, this would be even better :) +- I was not able to write a regression test for this, since the error seems to only occur under heavy load with special conditions. So please have a look for timeouts with the js parser, if you use it and report all issues and switch to the hiredis parser in the meanwhile. If you're able to come up with a reproducible test case, this would be even better :) - Fixed should_buffer boolean for .exec, .select and .auth commands not being returned and fix a couple special conditions ([@BridgeAR](https://github.com/BridgeAR)) If you do not rely on transactions but want to reduce the RTT you can use .batch from now on. It'll behave just the same as .multi but it does not have any transaction and therefor won't roll back any failed commands.
@@ -518,7 +518,7 @@ Bugfixes: - Fix argument mutation while using the array notation with the multi constructor (@BridgeAR) - Fix multi.hmset key not being type converted if used with an object and key not being a string (@BridgeAR) -- Fix parser errors not being catched properly (@BridgeAR) +- Fix parser errors not being caught properly (@BridgeAR) - Fix a crash that could occur if a redis server does not return the info command as usual #541 (@BridgeAR) - Explicitly passing undefined as a callback statement will work again. E.g. client.publish('channel', 'message', undefined); (@BridgeAR) @@ -560,13 +560,13 @@ This is the biggest release that node_redis had since it was released in 2010. A - Increased coverage by 10% and add a lot of tests to make sure everything works as it should. We now reached 97% :-) (@BridgeAR) - Remove dead code, clean up and refactor very old chunks (@BridgeAR) - Don't flush the offline queue if reconnecting (@BridgeAR) -- Emit all errors insteaf of throwing sometimes and sometimes emitting them (@BridgeAR) +- Emit all errors instead of throwing sometimes and sometimes emitting them (@BridgeAR) - _auth_pass_ passwords are now checked to be a valid password (@jcppman & @BridgeAR) ## Bug fixes: - Don't kill the app anymore by randomly throwing errors sync instead of emitting them (@BridgeAR) -- Don't catch user errors anymore occuring in callbacks (no try callback anymore & more fixes for the parser) (@BridgeAR) +- Don't catch user errors anymore occurring in callbacks (no try callback anymore & more fixes for the parser) (@BridgeAR) - Early garbage collection of queued items (@dohse) - Fix js parser returning errors as strings (@BridgeAR) - Do not wrap errors into other errors (@BridgeAR) @@ -588,19 +588,19 @@ This is the biggest release that node_redis had since it was released in 2010. A ## Breaking changes: 1. redis.send_command commands have to be lower case from now on. This does only apply if you use `.send_command` directly instead of the convenient methods like `redis.command`. -2. Error messages have changed quite a bit. If you depend on a specific wording please check your application carfully. +2. Error messages have changed quite a bit. If you depend on a specific wording please check your application carefully. 3. Errors are from now on always either returned if a callback is present or emitted. They won't be thrown (neither sync, nor async). 4. The Multi error handling has changed a lot! - All errors are from now on errors instead of strings (this only applied to the js parser). - If an error occurs while queueing the commands an EXECABORT error will be returned including the failed commands as `.errors` property instead of an array with errors. - If an error occurs while executing the commands and that command has a callback it'll return the error as first parameter (`err, undefined` instead of `null, undefined`). -- All the errors occuring while executing the commands will stay in the result value as error instance (if you used the js parser before they would have been strings). Be aware that the transaction won't be aborted if those error occurr! +- All the errors occurring while executing the commands will stay in the result value as error instance (if you used the js parser before they would have been strings). Be aware that the transaction won't be aborted if those error occur! - If `multi.exec` does not have a callback and an EXECABORT error occurrs, it'll emit that error instead. 5. If redis can't connect to your redis server it'll give up after a certain point of failures (either max connection attempts or connection timeout exceeded). If that is the case it'll emit an CONNECTION_BROKEN error. You'll have to initiate a new client to try again afterwards. 6. The offline queue is not flushed anymore on a reconnect. It'll stay until node_redis gives up trying to reach the server or until you close the connection. -7. Before this release node_redis catched user errors and threw them async back. This is not the case anymore! No user behavior of what so ever will be tracked or catched. +7. Before this release node_redis caught user errors and threw them async back. This is not the case anymore! No user behavior of what so ever will be tracked or caught. 8. The keyspace of `redis.server_info` (db0...) is from now on an object instead of an string. NodeRedis also thanks @qdb, @tobek, @cvibhagool, @frewsxcv, @davidbanham, @serv, @vitaliylag, @chrishamant, @GamingCoder and all other contributors that I may have missed for their contributions! From bd5c230c629a629f87a676b5687abb5056202eb5 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 11:29:59 +0300 Subject: [PATCH 090/105] fix: update package-lock.json (#2939) --- package-lock.json | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index dee35a0fcec..443d0d9e025 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8520,7 +8520,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8529,12 +8529,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" } }, "packages/client": { "name": "@redis/client", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -8550,7 +8550,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -8569,7 +8569,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" } }, "packages/entraid/node_modules/@types/node": { @@ -8606,7 +8606,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8615,18 +8615,18 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" } }, "packages/redis": { - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "dependencies": { - "@redis/bloom": "5.0.0-next.7", - "@redis/client": "5.0.0-next.7", - "@redis/json": "5.0.0-next.7", - "@redis/search": "5.0.0-next.7", - "@redis/time-series": "5.0.0-next.7" + "@redis/bloom": "5.0.0", + "@redis/client": "5.0.0", + "@redis/json": "5.0.0", + "@redis/search": "5.0.0", + "@redis/time-series": "5.0.0" }, "engines": { "node": ">= 18" @@ -8634,7 +8634,7 @@ }, "packages/search": { "name": "@redis/search", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8643,7 +8643,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" } }, "packages/test-utils": { @@ -8712,7 +8712,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.0.0-next.7", + "version": "5.0.0", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8721,7 +8721,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0-next.7" + "@redis/client": "^5.0.0" } } } From 2c9ad2e772fc444c2cbd0c4119d9bfbe399e2213 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 11:35:41 +0300 Subject: [PATCH 091/105] chore(examples): fix examples for v5 (#2938) --- examples/README.md | 14 +++++----- examples/blocking-list-pop.js | 5 ++-- examples/command-with-modifiers.js | 2 +- examples/connect-to-cluster.js | 5 ++-- examples/dump-and-restore.js | 6 ++++ examples/get-server-time.js | 10 +++++-- examples/hyperloglog.js | 4 +-- examples/lua-multi-incr.js | 8 ++++-- examples/managing-json.js | 11 ++++++-- examples/package.json | 4 +-- examples/search-hashes.js | 8 +++--- examples/search-json.js | 16 +++++------ examples/search-knn.js | 6 ++-- examples/set-scan.js | 12 ++++++-- examples/sorted-set.js | 28 +++++++++++++++++-- examples/stream-consumer-group.js | 16 +++++------ examples/time-series.js | 10 +++---- examples/topk.js | 12 ++++---- .../transaction-with-arbitrary-commands.js | 27 ++++++++++++------ examples/transaction-with-watch.js | 4 ++- 20 files changed, 133 insertions(+), 75 deletions(-) diff --git a/examples/README.md b/examples/README.md index 1080f424da1..47c9912fd31 100644 --- a/examples/README.md +++ b/examples/README.md @@ -40,12 +40,12 @@ We'd love to see more examples here. If you have an idea that you'd like to see To set up the examples folder so that you can run an example / develop one of your own: -``` -$ git clone https://github.com/redis/node-redis.git -$ cd node-redis -$ npm install -ws && npm run build-all -$ cd examples -$ npm install +```bash +git clone https://github.com/redis/node-redis.git +cd node-redis +npm install -ws && npm run build +cd examples +npm install ``` ### Coding Guidelines for Examples @@ -91,5 +91,5 @@ await client.connect(); // Add your example code here... -client.destroy(); +client.close(); ``` diff --git a/examples/blocking-list-pop.js b/examples/blocking-list-pop.js index ec9bec4d639..f29a409398c 100644 --- a/examples/blocking-list-pop.js +++ b/examples/blocking-list-pop.js @@ -4,16 +4,15 @@ // The script will be blocked until the LPUSH command is executed. // After which we log the list and quit the client. -import { createClient, commandOptions } from 'redis'; +import { createClientPool } from 'redis'; -const client = createClient(); +const client = createClientPool(); await client.connect(); const keyName = 'keyName'; const blpopPromise = client.blPop( - commandOptions({ isolated: true }), keyName, 0 ); diff --git a/examples/command-with-modifiers.js b/examples/command-with-modifiers.js index 31106b17e45..356304722c0 100644 --- a/examples/command-with-modifiers.js +++ b/examples/command-with-modifiers.js @@ -1,7 +1,7 @@ // Define a custom script that shows example of SET command // with several modifiers. -import { createClient } from '../packages/client'; +import { createClient } from 'redis'; const client = createClient(); diff --git a/examples/connect-to-cluster.js b/examples/connect-to-cluster.js index 98655497c9e..86e45b87968 100644 --- a/examples/connect-to-cluster.js +++ b/examples/connect-to-cluster.js @@ -1,7 +1,7 @@ // This is an example script to connect to a running cluster. // After connecting to the cluster the code sets and get a value. -// To setup this cluster you can follow the guide here: +// To setup this cluster you can follow the guide here: // https://redis.io/docs/manual/scaling/ // In this guide the ports which are being used are 7000 - 7005 @@ -29,5 +29,4 @@ await cluster.connect(); await cluster.set('hello', 'cluster'); const value = await cluster.get('hello'); console.log(value); - -await cluster.quit(); +await cluster.close(); diff --git a/examples/dump-and-restore.js b/examples/dump-and-restore.js index c2ee7f1e199..f464fd38be1 100644 --- a/examples/dump-and-restore.js +++ b/examples/dump-and-restore.js @@ -12,6 +12,12 @@ const client = await createClient({ console.log('Redis Client Error', err); }).connect(); +// Make sure the source key exists +await client.set('source', 'value'); + +// Make sure destination doesnt exist +await client.del('destination'); + // DUMP a specific key into a local variable const dump = await client.dump('source'); diff --git a/examples/get-server-time.js b/examples/get-server-time.js index 0e32c1296af..752264df349 100644 --- a/examples/get-server-time.js +++ b/examples/get-server-time.js @@ -6,7 +6,13 @@ const client = createClient(); await client.connect(); const serverTime = await client.time(); -// 2022-02-25T12:57:40.000Z { microseconds: 351346 } +// In v5, TIME returns [unixTimestamp: string, microseconds: string] instead of Date +// Example: ['1708956789', '123456'] console.log(serverTime); -client.destroy(); +// Convert to JavaScript Date if needed +const [seconds, microseconds] = serverTime; +const date = new Date(parseInt(seconds) * 1000 + parseInt(microseconds) / 1000); +console.log('Converted to Date:', date); + +client.close(); diff --git a/examples/hyperloglog.js b/examples/hyperloglog.js index 027112a08bf..1f8f04f2a6c 100644 --- a/examples/hyperloglog.js +++ b/examples/hyperloglog.js @@ -9,7 +9,7 @@ const client = createClient(); await client.connect(); // Use `pfAdd` to add an element to a Hyperloglog, creating the Hyperloglog if necessary. -// await client.pfAdd(key, value) +// await client.pfAdd(key, value) // returns 1 or 0 // To get a count, the `pfCount` method is used. // await client.pfCount(key) @@ -48,4 +48,4 @@ try { console.error(e); } -client.destroy(); +client.close(); diff --git a/examples/lua-multi-incr.js b/examples/lua-multi-incr.js index 5cf39142006..71b12bdab0f 100644 --- a/examples/lua-multi-incr.js +++ b/examples/lua-multi-incr.js @@ -12,9 +12,11 @@ const client = createClient({ 'redis.pcall("INCRBY", KEYS[1], ARGV[1]),' + 'redis.pcall("INCRBY", KEYS[2], ARGV[1])' + '}', - transformArguments(key1, key2, increment) { - return [key1, key2, increment.toString()]; - }, + parseCommand(parser, key1, key2, increment) { + parser.pushKey(key1); + parser.pushKey(key2); + parser.push(increment.toString()); + }, }), }, }); diff --git a/examples/managing-json.js b/examples/managing-json.js index a28a0ee5106..0f1eb3b3c21 100644 --- a/examples/managing-json.js +++ b/examples/managing-json.js @@ -57,7 +57,7 @@ results = await client.json.get('noderedis:jsondata', { }); // Goldie is 3 years old now. -console.log(`Goldie is ${JSON.stringify(results[0])} years old now.`); +console.log(`Goldie is ${JSON.parse(results)[0]} years old now.`); // Add a new pet... await client.json.arrAppend('noderedis:jsondata', '$.pets', { @@ -68,9 +68,14 @@ await client.json.arrAppend('noderedis:jsondata', '$.pets', { }); // How many pets do we have now? -const numPets = await client.json.arrLen('noderedis:jsondata', '$.pets'); +const numPets = await client.json.arrLen('noderedis:jsondata', { path: '$.pets' }); // We now have 4 pets. console.log(`We now have ${numPets} pets.`); -client.destroy(); +const rex = { name: 'Rex', species: 'dog', age: 3, isMammal: true } + +const index = await client.json.arrIndex( 'noderedis:jsondata', '$.pets', rex); +console.log(`Rex is at index ${index}`); + +client.close(); diff --git a/examples/package.json b/examples/package.json index 91120774d94..c350c0b248b 100644 --- a/examples/package.json +++ b/examples/package.json @@ -1,12 +1,12 @@ { "name": "node-redis-examples", "version": "1.0.0", - "description": "node-redis 4 example script", + "description": "node-redis 5 example script", "main": "index.js", "private": true, "type": "module", "dependencies": { - "redis": "../packages/client" + "redis": "../packages/redis" } } diff --git a/examples/search-hashes.js b/examples/search-hashes.js index f3aca6b8aed..a496fec823a 100644 --- a/examples/search-hashes.js +++ b/examples/search-hashes.js @@ -1,7 +1,7 @@ // This example demonstrates how to use RediSearch to index and query data // stored in Redis hashes. -import { createClient, SchemaFieldTypes } from 'redis'; +import { createClient, SCHEMA_FIELD_TYPE } from 'redis'; const client = createClient(); @@ -12,11 +12,11 @@ try { // Documentation: https://redis.io/commands/ft.create/ await client.ft.create('idx:animals', { name: { - type: SchemaFieldTypes.TEXT, + type: SCHEMA_FIELD_TYPE.TEXT, SORTABLE: true }, - species: SchemaFieldTypes.TAG, - age: SchemaFieldTypes.NUMERIC + species: SCHEMA_FIELD_TYPE.TAG, + age: SCHEMA_FIELD_TYPE.NUMERIC }, { ON: 'HASH', PREFIX: 'noderedis:animals' diff --git a/examples/search-json.js b/examples/search-json.js index bff5b2cb362..60f2ff095ed 100644 --- a/examples/search-json.js +++ b/examples/search-json.js @@ -3,7 +3,7 @@ // https://redis.io/docs/stack/search/ // https://redis.io/docs/stack/json/ -import { createClient, SchemaFieldTypes, AggregateGroupByReducers, AggregateSteps } from 'redis'; +import { createClient, SCHEMA_FIELD_TYPE, FT_AGGREGATE_GROUP_BY_REDUCERS, FT_AGGREGATE_STEPS } from 'redis'; const client = createClient(); @@ -14,19 +14,19 @@ await client.connect(); try { await client.ft.create('idx:users', { '$.name': { - type: SchemaFieldTypes.TEXT, + type: SCHEMA_FIELD_TYPE.TEXT, SORTABLE: true }, '$.age': { - type: SchemaFieldTypes.NUMERIC, + type: SCHEMA_FIELD_TYPE.NUMERIC, AS: 'age' }, '$.coins': { - type: SchemaFieldTypes.NUMERIC, + type: SCHEMA_FIELD_TYPE.NUMERIC, AS: 'coins' }, '$.email': { - type: SchemaFieldTypes.TAG, + type: SCHEMA_FIELD_TYPE.TAG, AS: 'email' } }, { @@ -119,13 +119,13 @@ console.log( JSON.stringify( await client.ft.aggregate('idx:users', '*', { STEPS: [{ - type: AggregateSteps.GROUPBY, + type: FT_AGGREGATE_STEPS.GROUPBY, REDUCE: [{ - type: AggregateGroupByReducers.AVG, + type: FT_AGGREGATE_GROUP_BY_REDUCERS.AVG, property: 'age', AS: 'averageAge' }, { - type: AggregateGroupByReducers.SUM, + type: FT_AGGREGATE_GROUP_BY_REDUCERS.SUM, property: 'coins', AS: 'totalCoins' }] diff --git a/examples/search-knn.js b/examples/search-knn.js index 49bd00d86df..abfce990189 100644 --- a/examples/search-knn.js +++ b/examples/search-knn.js @@ -4,7 +4,7 @@ // Inspired by RediSearch Python tests: // https://github.com/RediSearch/RediSearch/blob/06e36d48946ea08bd0d8b76394a4e82eeb919d78/tests/pytests/test_vecsim.py#L96 -import { createClient, SchemaFieldTypes, VectorAlgorithms } from 'redis'; +import { createClient, SCHEMA_FIELD_TYPE, SCHEMA_VECTOR_FIELD_ALGORITHM } from 'redis'; const client = createClient(); @@ -15,8 +15,8 @@ try { // Documentation: https://redis.io/docs/stack/search/reference/vectors/ await client.ft.create('idx:knn-example', { v: { - type: SchemaFieldTypes.VECTOR, - ALGORITHM: VectorAlgorithms.HNSW, + type: SCHEMA_FIELD_TYPE.VECTOR, + ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.HNSW, TYPE: 'FLOAT32', DIM: 2, DISTANCE_METRIC: 'COSINE' diff --git a/examples/set-scan.js b/examples/set-scan.js index 0e379224d9d..698b05983b0 100644 --- a/examples/set-scan.js +++ b/examples/set-scan.js @@ -8,8 +8,14 @@ const client = createClient(); await client.connect(); const setName = 'setName'; -for await (const member of client.sScanIterator(setName)) { - console.log(member); + +for await (const members of client.sScanIterator(setName)) { + console.log('Batch of members:', members); + + // Process each member in the batch if needed + for (const member of members) { + console.log('Individual member:', member); + } } -client.destroy(); +client.close(); diff --git a/examples/sorted-set.js b/examples/sorted-set.js index 3fcc24b8442..830427bea9a 100644 --- a/examples/sorted-set.js +++ b/examples/sorted-set.js @@ -24,8 +24,30 @@ await client.zAdd('mysortedset', [ // Get all of the values/scores from the sorted set using // the scan approach: // https://redis.io/commands/zscan -for await (const memberWithScore of client.zScanIterator('mysortedset')) { - console.log(memberWithScore); +for await (const membersWithScores of client.zScanIterator('mysortedset')) { + console.log('Batch of members with scores:', membersWithScores); + + for (const memberWithScore of membersWithScores) { + console.log('Individual member with score:', memberWithScore); + } } -client.destroy(); +await client.zAdd('anothersortedset', [ + { + score: 99, + value: 'Ninety Nine' + }, + { + score: 102, + value: 'One Hundred and Two' + } +]); + +// Intersection of two sorted sets +const intersection = await client.zInter([ + { key: 'mysortedset', weight: 1 }, + { key: 'anothersortedset', weight: 1 } +]); +console.log('Intersection:', intersection); + +client.close(); diff --git a/examples/stream-consumer-group.js b/examples/stream-consumer-group.js index 0161b5b4d32..cf82b5e96af 100644 --- a/examples/stream-consumer-group.js +++ b/examples/stream-consumer-group.js @@ -20,7 +20,7 @@ // // $ node stream-consumer-group.js consumer2 -import { createClient, commandOptions } from 'redis'; +import { createClient } from 'redis'; const client = createClient(); @@ -46,14 +46,13 @@ try { console.log(`Starting consumer ${consumerName}.`); +const pool = client.createPool(); + while (true) { try { // https://redis.io/commands/xreadgroup/ - let response = await client.xReadGroup( - commandOptions({ - isolated: true - }), - 'myconsumergroup', + let response = await pool.xReadGroup( + 'myconsumergroup', consumerName, [ // XREADGROUP can read from multiple streams, starting at a // different ID for each... @@ -91,9 +90,10 @@ while (true) { // stream entry. // https://redis.io/commands/xack/ const entryId = response[0].messages[0].id; - await client.xAck('mystream', 'myconsumergroup', entryId); + const ackResult = await pool.xAck('mystream', 'myconsumergroup', entryId); - console.log(`Acknowledged processing of entry ${entryId}.`); + // ackResult will be 1 if the message was successfully acknowledged, 0 otherwise + console.log(`Acknowledged processing of entry ${entryId}. Result: ${ackResult}`); } else { // Response is null, we have read everything that is // in the stream right now... diff --git a/examples/time-series.js b/examples/time-series.js index 1d61ff94408..75df2736f81 100644 --- a/examples/time-series.js +++ b/examples/time-series.js @@ -2,7 +2,7 @@ // Requires the RedisTimeSeries module: https://redis.io/docs/stack/timeseries/ import { createClient } from 'redis'; -import { TimeSeriesDuplicatePolicies, TimeSeriesEncoding, TimeSeriesAggregationType } from '@redis/time-series'; +import { TIME_SERIES_DUPLICATE_POLICIES, TIME_SERIES_ENCODING, TIME_SERIES_AGGREGATION_TYPE } from '@redis/time-series'; const client = createClient(); @@ -14,8 +14,8 @@ try { // https://redis.io/commands/ts.create/ const created = await client.ts.create('mytimeseries', { RETENTION: 86400000, // 1 day in milliseconds - ENCODING: TimeSeriesEncoding.UNCOMPRESSED, // No compression - DUPLICATE_POLICY: TimeSeriesDuplicatePolicies.BLOCK // No duplicates + ENCODING: TIME_SERIES_ENCODING.UNCOMPRESSED, // No compression + DUPLICATE_POLICY: TIME_SERIES_DUPLICATE_POLICIES.BLOCK // No duplicates }); if (created === 'OK') { @@ -74,7 +74,7 @@ try { const rangeResponse = await client.ts.range('mytimeseries', fromTimestamp, toTimestamp, { // Group into 10 second averages. AGGREGATION: { - type: TimeSeriesAggregationType.AVERAGE, + type: TIME_SERIES_AGGREGATION_TYPE.AVG, timeBucket: 10000 } }); @@ -119,4 +119,4 @@ try { console.error(e); } -client.destroy(); +client.close(); diff --git a/examples/topk.js b/examples/topk.js index d09144c230c..10cc29950ed 100644 --- a/examples/topk.js +++ b/examples/topk.js @@ -1,4 +1,4 @@ -// This example demonstrates the use of the Top K +// This example demonstrates the use of the Top K // in the RedisBloom module (https://redis.io/docs/stack/bloom/) import { createClient } from 'redis'; @@ -95,10 +95,10 @@ const [ steve, suze, leibale, frederick ] = await client.topK.query('mytopk', [ 'frederick' ]); -console.log(`steve ${steve === 1 ? 'is': 'is not'} in the top 10.`); -console.log(`suze ${suze === 1 ? 'is': 'is not'} in the top 10.`); -console.log(`leibale ${leibale === 1 ? 'is': 'is not'} in the top 10.`); -console.log(`frederick ${frederick === 1 ? 'is': 'is not'} in the top 10.`); +console.log(`steve ${steve ? 'is': 'is not'} in the top 10.`); +console.log(`suze ${suze ? 'is': 'is not'} in the top 10.`); +console.log(`leibale ${leibale ? 'is': 'is not'} in the top 10.`); +console.log(`frederick ${frederick ? 'is': 'is not'} in the top 10.`); // Get count estimate for some team members with TOPK.COUNT: // https://redis.io/commands/topk.count/ @@ -110,4 +110,4 @@ const [ simonCount, lanceCount ] = await client.topK.count('mytopk', [ console.log(`Count estimate for simon: ${simonCount}.`); console.log(`Count estimate for lance: ${lanceCount}.`); -client.destroy(); +client.close(); diff --git a/examples/transaction-with-arbitrary-commands.js b/examples/transaction-with-arbitrary-commands.js index d68533205a1..cc22a659678 100644 --- a/examples/transaction-with-arbitrary-commands.js +++ b/examples/transaction-with-arbitrary-commands.js @@ -1,6 +1,6 @@ -// How to mix and match supported commands that have named functions with +// How to mix and match supported commands that have named functions with // commands sent as arbitrary strings in the same transaction context. -// Use this when working with new Redis commands that haven't been added to +// Use this when working with new Redis commands that haven't been added to // node-redis yet, or when working with commands that have been added to Redis // by modules other than those directly supported by node-redis. @@ -23,18 +23,29 @@ await client.sendCommand(['hset', 'hash2', 'number', '3']); // In a transaction context, use addCommand to send arbitrary commands. // addCommand can be mixed and matched with named command functions as // shown. -const responses = await client - .multi() +const multi = client.multi() .hGetAll('hash2') .addCommand(['hset', 'hash3', 'number', '4']) - .hGet('hash3', 'number') - .exec(); + .hGet('hash3', 'number'); + +// exec() returns Array +const responses = await multi.exec(); // responses will be: // [ [Object: null prototype] { name: 'hash2', number: '3' }, 0, '4' ] -console.log(responses); +console.log('Using exec():', responses); + +// This is equivalent to multi.exec<'typed'>() +const typedResponses = await multi + .hGetAll('hash2') + .addCommand(['hset', 'hash3', 'number', '4']) + .hGet('hash3', 'number') + .execTyped(); + +// typedResponses will have more specific types +console.log('Using execTyped():', typedResponses); // Clean up fixtures. await client.del(['hash1', 'hash2', 'hash3']); -client.destroy(); +client.close(); diff --git a/examples/transaction-with-watch.js b/examples/transaction-with-watch.js index d92b910dfa3..752d0b6a4e3 100644 --- a/examples/transaction-with-watch.js +++ b/examples/transaction-with-watch.js @@ -13,9 +13,11 @@ function restrictFunctionCalls(fn, maxCalls) { const fn = restrictFunctionCalls(transaction, 4); +const pool = await client.createPool(); + async function transaction() { try { - await client.executeIsolated(async (isolatedClient) => { + await pool.execute(async (isolatedClient) => { await isolatedClient.watch('paymentId:1259'); const multi = isolatedClient .multi() From f6912b03da8f8d8885714369d71df28d9a48ec56 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 11:38:51 +0300 Subject: [PATCH 092/105] Update packages/redis/README.md (#2935) * copy root readme into redis readme * fix links * update supported versions * update supported versions --- README.md | 8 +- packages/redis/README.md | 322 ++++++++++++++++++++++++++------------- 2 files changed, 219 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index b9fe524c677..ac6394461f1 100644 --- a/README.md +++ b/README.md @@ -288,11 +288,9 @@ Node Redis is supported with the following versions of Redis: | Version | Supported | | ------- | ------------------ | | 8.0.z | :heavy_check_mark: | -| 7.0.z | :heavy_check_mark: | -| 6.2.z | :heavy_check_mark: | -| 6.0.z | :heavy_check_mark: | -| 5.0.z | :heavy_check_mark: | -| < 5.0 | :x: | +| 7.4.z | :heavy_check_mark: | +| 7.2.z | :heavy_check_mark: | +| < 7.2 | :x: | > Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support. diff --git a/packages/redis/README.md b/packages/redis/README.md index 76929ffa48b..f0b2a34905d 100644 --- a/packages/redis/README.md +++ b/packages/redis/README.md @@ -1,195 +1,305 @@ # Node-Redis +[![Tests](https://img.shields.io/github/actions/workflow/status/redis/node-redis/tests.yml?branch=master)](https://github.com/redis/node-redis/actions/workflows/tests.yml) +[![Coverage](https://codecov.io/gh/redis/node-redis/branch/master/graph/badge.svg?token=xcfqHhJC37)](https://codecov.io/gh/redis/node-redis) +[![License](https://img.shields.io/github/license/redis/node-redis.svg)](https://github.com/redis/node-redis/blob/master/LICENSE) + +[![Discord](https://img.shields.io/discord/697882427875393627.svg?style=social&logo=discord)](https://discord.gg/redis) +[![Twitch](https://img.shields.io/twitch/status/redisinc?style=social)](https://www.twitch.tv/redisinc) +[![YouTube](https://img.shields.io/youtube/channel/views/UCD78lHSwYqMlyetR0_P4Vig?style=social)](https://www.youtube.com/redisinc) +[![Twitter](https://img.shields.io/twitter/follow/redisinc?style=social)](https://twitter.com/redisinc) + +node-redis is a modern, high performance [Redis](https://redis.io) client for Node.js. + +## How do I Redis? + +[Learn for free at Redis University](https://university.redis.com/) + +[Build faster with the Redis Launchpad](https://launchpad.redis.com/) + +[Try the Redis Cloud](https://redis.com/try-free/) + +[Dive in developer tutorials](https://developer.redis.com/) + +[Join the Redis community](https://redis.com/community/) + +[Work at Redis](https://redis.com/company/careers/jobs/) + +## Installation + +Start a redis via docker: + +```bash +docker run -p 6379:6379 -d redis:8.0-rc1 +``` + +To install node-redis, simply: + +```bash +npm install redis +``` +> "redis" is the "whole in one" package that includes all the other packages. If you only need a subset of the commands, +> you can install the individual packages. See the list below. + +## Packages + +| Name | Description | +| ---------------------------------------------- | ------------------------------------------------------------------------------------------- | +| [`redis`](../redis) | The client with all the ["redis-stack"](https://github.com/redis-stack/redis-stack) modules | +| [`@redis/client`](../client) | The base clients (i.e `RedisClient`, `RedisCluster`, etc.) | +| [`@redis/bloom`](../bloom) | [Redis Bloom](https://redis.io/docs/data-types/probabilistic/) commands | +| [`@redis/json`](../json) | [Redis JSON](https://redis.io/docs/data-types/json/) commands | +| [`@redis/search`](../search) | [RediSearch](https://redis.io/docs/interact/search-and-query/) commands | +| [`@redis/time-series`](../time-series) | [Redis Time-Series](https://redis.io/docs/data-types/timeseries/) commands | +| [`@redis/entraid`](../entraid) | Secure token-based authentication for Redis clients using Microsoft Entra ID | + +> Looking for a high-level library to handle object mapping? +> See [redis-om-node](https://github.com/redis/redis-om-node)! + + ## Usage ### Basic Example -```javascript -import { createClient } from 'redis'; +```typescript +import { createClient } from "redis"; const client = await createClient() - .on('error', err => console.log('Redis Client Error', err)) + .on("error", (err) => console.log("Redis Client Error", err)) .connect(); -await client.set('key', 'value'); -const value = await client.get('key'); -await client.close(); +await client.set("key", "value"); +const value = await client.get("key"); +client.destroy(); ``` -> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and an `error` occurs, that error will be thrown and the Node.js process will exit. See the [`EventEmitter` docs](https://nodejs.org/api/events.html#error-events) for more details. +The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in +the format `redis[s]://[[username][:password]@][host][:port][/db-number]`: -The above code connects to localhost on port 6379. To connect to a different host or port, use a connection string in the format `redis[s]://[[username][:password]@][host][:port][/db-number]`: - -```javascript +```typescript createClient({ - url: 'redis://alice:foobared@awesome.redis.server:6380' + url: "redis://alice:foobared@awesome.redis.server:6380", }); ``` -You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in the [client configuration guide](../../docs/client-configuration.md). +You can also use discrete parameters, UNIX sockets, and even TLS to connect. Details can be found in +the [client configuration guide](../../docs/client-configuration.md). + +To check if the the client is connected and ready to send commands, use `client.isReady` which returns a boolean. +`client.isOpen` is also available. This returns `true` when the client's underlying socket is open, and `false` when it +isn't (for example when the client is still connecting or reconnecting after a network error). ### Redis Commands -There is built-in support for all of the [out-of-the-box Redis commands](https://redis.io/commands). They are exposed using the raw Redis command names (`HSET`, `HGETALL`, etc.) and a friendlier camel-cased version (`hSet`, `hGetAll`, etc.): +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.): -```javascript +```typescript // raw Redis commands -await client.HSET('key', 'field', 'value'); -await client.HGETALL('key'); +await client.HSET("key", "field", "value"); +await client.HGETALL("key"); // friendly JavaScript commands -await client.hSet('key', 'field', 'value'); -await client.hGetAll('key'); +await client.hSet("key", "field", "value"); +await client.hGetAll("key"); ``` Modifiers to commands are specified using a JavaScript object: -```javascript -await client.set('key', 'value', { - expiration: { - type: 'EX', - value: 10 - }, - condition: 'NX' +```typescript +await client.set("key", "value", { + EX: 10, + NX: true, }); ``` -> NOTE: command modifiers that change the reply type (e.g. `WITHSCORES` for `ZDIFF`) are exposed as separate commands (e.g. `ZDIFF_WITHSCORES`/`zDiffWithScores`). - -Replies will be mapped to useful data structures: +Replies will be transformed into useful data structures: -```javascript -await client.hGetAll('key'); // { field1: 'value1', field2: 'value2' } -await client.hVals('key'); // ['value1', 'value2'] +```typescript +await client.hGetAll("key"); // { field1: 'value1', field2: 'value2' } +await client.hVals("key"); // ['value1', 'value2'] ``` -> NOTE: you can change the default type mapping. See the [Type Mapping](../../docs/command-options.md#type-mapping) documentation for more information. +`Buffer`s are supported as well: + +```typescript +const client = createClient().withTypeMapping({ + [RESP_TYPES.BLOB_STRING]: Buffer +}); + +await client.hSet("key", "field", Buffer.from("value")); // 'OK' +await client.hGet("key", "field"); // { field: } + +``` ### Unsupported Redis Commands If you want to run commands and/or use arguments that Node Redis doesn't know about (yet!) use `.sendCommand()`: -```javascript -await client.sendCommand(['SET', 'key', 'value', 'EX', '10', 'NX']); // 'OK' -await client.sendCommand(['HGETALL', 'key']); // ['key1', 'field1', 'key2', 'field2'] +```typescript +await client.sendCommand(["SET", "key", "value", "NX"]); // 'OK' + +await client.sendCommand(["HGETALL", "key"]); // ['key1', 'field1', 'key2', 'field2'] ``` -### Disconnecting +### Transactions (Multi/Exec) -#### `.close()` +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: -Gracefully close a client's connection to Redis. -Wait for commands in process, but reject any new commands. +```typescript +await client.set("another-key", "another-value"); -```javascript -const [ping, get] = await Promise.all([ - client.ping(), - client.get('key'), - client.close() -]); // ['PONG', null] - -try { - await client.get('key'); -} catch (err) { - // ClientClosedError -} +const [setKeyReply, otherKeyValue] = await client + .multi() + .set("key", "value") + .get("another-key") + .exec(); // ['OK', 'another-value'] ``` -> `.close()` is just like `.quit()` which was depreacted v5. See the [relevant section in the migration guide](../../docs/v4-to-v5.md#Quit-VS-Disconnect) for more information. +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. + -#### `.destroy()` +### Blocking Commands -Forcibly close a client's connection to Redis. +In v4, `RedisClient` had the ability to create a pool of connections using an "Isolation Pool" on top of the "main" +connection. However, there was no way to use the pool without a "main" connection: ```javascript -try { - const promise = Promise.all([ - client.ping(), - client.get('key') - ]); +const client = await createClient() + .on("error", (err) => console.error(err)) + .connect(); - client.destroy(); +await client.ping(client.commandOptions({ isolated: true })); +``` + +In v5 we've extracted this pool logic into its own class—`RedisClientPool`: + +```javascript +const pool = await createClientPool() + .on("error", (err) => console.error(err)) + .connect(); - await promise; -} catch (err) { - // DisconnectsClientError +await pool.ping(); +``` + + +### Pub/Sub + +See the [Pub/Sub overview](../../docs/pub-sub.md). + +### Scan Iterator + +[`SCAN`](https://redis.io/commands/scan) results can be looped over +using [async iterators](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/asyncIterator): + +```typescript +for await (const key of client.scanIterator()) { + // use the key! + await client.get(key); } +``` + +This works with `HSCAN`, `SSCAN`, and `ZSCAN` too: -try { - await client.get('key'); -} catch (err) { - // ClientClosedError +```typescript +for await (const { field, value } of client.hScanIterator("hash")) { +} +for await (const member of client.sScanIterator("set")) { +} +for await (const { score, value } of client.zScanIterator("sorted-set")) { } ``` -> `.destroy()` is just like `.disconnect()` which was depreated in v5. See the [relevant section in the migration guide](../../docs/v4-to-v5.md#Quit-VS-Disconnect) for more information. +You can override the default options by providing a configuration object: + +```typescript +client.scanIterator({ + TYPE: "string", // `SCAN` only + MATCH: "patter*", + COUNT: 100, +}); +``` + +### Disconnecting + +The `QUIT` command has been deprecated in Redis 7.2 and should now also be considered deprecated in Node-Redis. Instead +of sending a `QUIT` command to the server, the client can simply close the network connection. + +`client.QUIT/quit()` is replaced by `client.close()`. and, to avoid confusion, `client.disconnect()` has been renamed to +`client.destroy()`. + +```typescript +client.destroy(); +``` ### Auto-Pipelining Node Redis will automatically pipeline requests that are made during the same "tick". -```javascript -client.set('Tm9kZSBSZWRpcw==', 'users:1'); -client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw=='); +```typescript +client.set("Tm9kZSBSZWRpcw==", "users:1"); +client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw=="); ``` -Of course, if you don't do something with your Promises you're certain to get [unhandled Promise exceptions](https://nodejs.org/api/process.html#process_event_unhandledrejection). To take advantage of auto-pipelining and handle your Promises, use `Promise.all()`. +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()`. -```javascript +```typescript await Promise.all([ - client.set('Tm9kZSBSZWRpcw==', 'users:1'), - client.sAdd('users:1:tokens', 'Tm9kZSBSZWRpcw==') + client.set("Tm9kZSBSZWRpcw==", "users:1"), + client.sAdd("users:1:tokens", "Tm9kZSBSZWRpcw=="), ]); ``` -### Connection State +### Programmability -To client exposes 2 `boolean`s that track the client state: -1. `isOpen` - the client is either connecting or connected. -2. `isReady` - the client is connected and ready to send +See the [Programmability overview](../../docs/programmability.md). -### Events +### Clustering -The client extends `EventEmitter` and emits the following events: +Check out the [Clustering Guide](../../docs/clustering.md) when using Node Redis to connect to a Redis Cluster. -| Name | When | Listener arguments | -|-------------------------|------------------------------------------------------------------------------------|------------------------------------------------------------| -| `connect` | Initiating a connection to the server | *No arguments* | -| `ready` | Client is ready to use | *No arguments* | -| `end` | Connection has been closed (via `.quit()` or `.disconnect()`) | *No arguments* | -| `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` | -| `reconnecting` | Client is trying to reconnect to the server | *No arguments* | -| `sharded-channel-moved` | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | +### Events + +The Node Redis client class is an Nodejs EventEmitter and it emits an event each time the network status changes: -> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and an `error` occurs, that error will be thrown and the Node.js process will exit. See the [`EventEmitter` docs](https://nodejs.org/api/events.html#error-events) for more details. +| Name | When | Listener arguments | +| ----------------------- | ---------------------------------------------------------------------------------- | --------------------------------------------------------- | +| `connect` | Initiating a connection to the server | _No arguments_ | +| `ready` | Client is ready to use | _No arguments_ | +| `end` | Connection has been closed (via `.disconnect()`) | _No arguments_ | +| `error` | An error has occurred—usually a network issue such as "Socket closed unexpectedly" | `(error: Error)` | +| `reconnecting` | Client is trying to reconnect to the server | _No arguments_ | +| `sharded-channel-moved` | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | See [here](../../docs/pub-sub.md#sharded-channel-moved-event) | -### Read more +> :warning: You **MUST** listen to `error` events. If a client doesn't have at least one `error` listener registered and +> an `error` occurs, that error will be thrown and the Node.js process will exit. See the [ > `EventEmitter` docs](https://nodejs.org/api/events.html#events_error_events) for more details. -- [Transactions (`MULTI`/`EXEC`)](../../docs/transactions.md). -- [Pub/Sub](../../docs/pub-sub.md). -- [Scan Iterators](../../docs/scan-iterators.md). -- [Programmability](../../docs/programmability.md). -- [Command Options](../../docs/command-options.md). -- [Pool](../../docs/pool.md). -- [Clustering](../../docs/clustering.md). -- [Sentinel](../../docs/sentinel.md). -- [FAQ](../../docs/FAQ.md). +> The client will not emit [any other events](../../docs/v3-to-v4.md#all-the-removed-events) beyond those listed above. ## Supported Redis versions Node Redis is supported with the following versions of Redis: | Version | Supported | -|---------|--------------------| +| ------- | ------------------ | +| 8.0.z | :heavy_check_mark: | +| 7.4.z | :heavy_check_mark: | | 7.2.z | :heavy_check_mark: | -| 7.0.z | :heavy_check_mark: | -| 6.2.z | :heavy_check_mark: | -| 6.0.z | :heavy_check_mark: | -| 5.0.z | :heavy_check_mark: | -| < 5.0 | :x: | +| < 7.2 | :x: | > Node Redis should work with older versions of Redis, but it is not fully tested and we cannot offer support. +## Migration + +- [From V3 to V4](../../docs/v3-to-v4.md) +- [From V4 to V5](../../docs/v4-to-v5.md) +- [V5](../../docs/v5.md) + ## Contributing If you'd like to contribute, check out the [contributing guide](../../CONTRIBUTING.md). From 404db308374ed1bb61dca6f62826e4a7d6f87d84 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 11:51:15 +0300 Subject: [PATCH 093/105] Release client@5.0.1 --- packages/client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/package.json b/packages/client/package.json index 5b88c185da9..34d60154083 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -1,6 +1,6 @@ { "name": "@redis/client", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 2c9faad2d9673941111018047dc420ecc1c842a0 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 11:55:40 +0300 Subject: [PATCH 094/105] Updated the Bloom package to use client@5.0.1 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 595e581ad8e..41a366c01d9 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" }, "devDependencies": { "@redis/test-utils": "*" From 6714ad109ddd00b9a4be968e84e40de19c5e5df1 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 11:57:11 +0300 Subject: [PATCH 095/105] Release bloom@5.0.1 --- packages/bloom/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/bloom/package.json b/packages/bloom/package.json index 41a366c01d9..0772fcd3bb8 100644 --- a/packages/bloom/package.json +++ b/packages/bloom/package.json @@ -1,6 +1,6 @@ { "name": "@redis/bloom", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 57e5daab98f5adcc4d7d9eaa2d7de0e1bb6296b5 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 11:58:23 +0300 Subject: [PATCH 096/105] Updated the Entraid package to use client@5.0.1 --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 8e9ff16aa22..665d3cc852e 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -21,7 +21,7 @@ "@azure/msal-node": "^2.16.1" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" }, "devDependencies": { "@types/express": "^4.17.21", From 67cde227ccc54ae1d606e3ee03aabdca5579cb66 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 11:59:07 +0300 Subject: [PATCH 097/105] Release entraid@5.0.1 --- packages/entraid/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/entraid/package.json b/packages/entraid/package.json index 665d3cc852e..57af37d36cd 100644 --- a/packages/entraid/package.json +++ b/packages/entraid/package.json @@ -1,6 +1,6 @@ { "name": "@redis/entraid", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts", From 84680d6e5a6176a2babf8a1d718b117293c7d74f Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 12:00:10 +0300 Subject: [PATCH 098/105] Updated the Json package to use client@5.0.1 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index 7d6163cc59e..ac946aa2381 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" }, "devDependencies": { "@redis/test-utils": "*" From e99cd073b66e4d2466c2bd2698ec7ae073635fac Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 12:00:46 +0300 Subject: [PATCH 099/105] Release json@5.0.1 --- packages/json/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/json/package.json b/packages/json/package.json index ac946aa2381..5c2dfc49a45 100644 --- a/packages/json/package.json +++ b/packages/json/package.json @@ -1,6 +1,6 @@ { "name": "@redis/json", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From cc13ae298e998830873b7a9db0268c018903250b Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 12:01:39 +0300 Subject: [PATCH 100/105] Updated the Search package to use client@5.0.1 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index 0097cce3480..df907d98972 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -13,7 +13,7 @@ "test-sourcemap": "mocha -r ts-node/register/transpile-only './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" }, "devDependencies": { "@redis/test-utils": "*" From 17179ddb351d5405aa47cd954303b52c0668345e Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 12:02:20 +0300 Subject: [PATCH 101/105] Release search@5.0.1 --- packages/search/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/search/package.json b/packages/search/package.json index df907d98972..ba1fa2a74be 100644 --- a/packages/search/package.json +++ b/packages/search/package.json @@ -1,6 +1,6 @@ { "name": "@redis/search", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From 1e976d24ccd0f6f8e5baf1eacf4cc871fc34dbef Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 12:03:17 +0300 Subject: [PATCH 102/105] Updated the Timeseries package to use client@5.0.1 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 5960350ff41..9d57f5558d2 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -12,7 +12,7 @@ "test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" }, "devDependencies": { "@redis/test-utils": "*" From 71ab009ef8d13a7ba33966d6bb0e5a79511f7ef4 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 12:03:48 +0300 Subject: [PATCH 103/105] Release time-series@5.0.1 --- packages/time-series/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/time-series/package.json b/packages/time-series/package.json index 9d57f5558d2..1b436701d6b 100644 --- a/packages/time-series/package.json +++ b/packages/time-series/package.json @@ -1,6 +1,6 @@ { "name": "@redis/time-series", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "main": "./dist/lib/index.js", "types": "./dist/lib/index.d.ts", From fd069641f30fb684516de2b478b6c5a3581058d0 Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 12:21:02 +0300 Subject: [PATCH 104/105] Updated the Redis package to use client@5.0.1 --- package-lock.json | 32 ++++++++++++++++---------------- packages/redis/package.json | 10 +++++----- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 443d0d9e025..0b8ca6ee37e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8520,7 +8520,7 @@ }, "packages/bloom": { "name": "@redis/bloom", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8529,12 +8529,12 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" } }, "packages/client": { "name": "@redis/client", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "dependencies": { "cluster-key-slot": "1.1.2" @@ -8550,7 +8550,7 @@ }, "packages/entraid": { "name": "@redis/entraid", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "dependencies": { "@azure/identity": "^4.7.0", @@ -8569,7 +8569,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" } }, "packages/entraid/node_modules/@types/node": { @@ -8606,7 +8606,7 @@ }, "packages/json": { "name": "@redis/json", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8615,18 +8615,18 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" } }, "packages/redis": { "version": "5.0.0", "license": "MIT", "dependencies": { - "@redis/bloom": "5.0.0", - "@redis/client": "5.0.0", - "@redis/json": "5.0.0", - "@redis/search": "5.0.0", - "@redis/time-series": "5.0.0" + "@redis/bloom": "5.0.1", + "@redis/client": "5.0.1", + "@redis/json": "5.0.1", + "@redis/search": "5.0.1", + "@redis/time-series": "5.0.1" }, "engines": { "node": ">= 18" @@ -8634,7 +8634,7 @@ }, "packages/search": { "name": "@redis/search", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8643,7 +8643,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" } }, "packages/test-utils": { @@ -8712,7 +8712,7 @@ }, "packages/time-series": { "name": "@redis/time-series", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "devDependencies": { "@redis/test-utils": "*" @@ -8721,7 +8721,7 @@ "node": ">= 18" }, "peerDependencies": { - "@redis/client": "^5.0.0" + "@redis/client": "^5.0.1" } } } diff --git a/packages/redis/package.json b/packages/redis/package.json index ddb79ca2275..ab894d5b0fe 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -10,11 +10,11 @@ "!dist/tsconfig.tsbuildinfo" ], "dependencies": { - "@redis/bloom": "5.0.0", - "@redis/client": "5.0.0", - "@redis/json": "5.0.0", - "@redis/search": "5.0.0", - "@redis/time-series": "5.0.0" + "@redis/bloom": "5.0.1", + "@redis/client": "5.0.1", + "@redis/json": "5.0.1", + "@redis/search": "5.0.1", + "@redis/time-series": "5.0.1" }, "engines": { "node": ">= 18" From 52e55623568684be0d6d16545cb938808422c5ea Mon Sep 17 00:00:00 2001 From: Nikolay Karadzhov Date: Mon, 5 May 2025 12:28:40 +0300 Subject: [PATCH 105/105] Release redis@5.0.1 --- packages/redis/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/redis/package.json b/packages/redis/package.json index ab894d5b0fe..e7c9da2660b 100644 --- a/packages/redis/package.json +++ b/packages/redis/package.json @@ -1,7 +1,7 @@ { "name": "redis", "description": "A modern, high performance Redis client", - "version": "5.0.0", + "version": "5.0.1", "license": "MIT", "main": "./dist/index.js", "types": "./dist/index.d.ts",